diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e33ba607e..c60a89e2f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APKs diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index b7912b2b72..aa00b74c44 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -61,7 +61,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug Gplay Enterprise APK diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index e4fedf76ac..55a300dd88 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -12,16 +12,18 @@ jobs: runs-on: ubuntu-latest # Skip in forks if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} + permissions: + contents: write steps: - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index f320230584..8903ed0e57 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -50,7 +50,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index ae5c72a3d9..314929d801 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -34,7 +34,7 @@ jobs: swap-storage: false - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 @@ -43,7 +43,7 @@ jobs: java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: false @@ -85,7 +85,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 7845be8323..9a191ba15b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -74,7 +74,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 @@ -113,7 +113,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Konsist tests @@ -154,7 +154,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run compose tests @@ -188,7 +188,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Build Gplay Debug @@ -233,7 +233,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Detekt @@ -274,7 +274,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Ktlint check diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 189ae26cf3..0f4c8ee581 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -43,13 +43,13 @@ jobs: labels: Record-Screenshots - name: ⏬ Checkout with LFS (PR) if: github.event.label.name == 'Record-Screenshots' - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 with: persist-credentials: false ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} - name: ⏬ Checkout with LFS (Branch) if: github.event_name == 'workflow_dispatch' - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 with: persist-credentials: false - name: ☕️ Use JDK 21 @@ -59,7 +59,7 @@ jobs: java-version: '21' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f9df37160..73cba6c8f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,7 +43,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} @@ -87,7 +87,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - name: Create Enterprise app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} @@ -131,7 +131,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 - name: Create APKs env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh index 51a968fdec..4ee021c316 100755 --- a/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh +++ b/.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh @@ -19,6 +19,9 @@ adb install -r $1 echo "Starting the screen recording..." adb push .github/workflows/scripts/maestro/local-recording.sh /data/local/tmp/ adb shell "chmod +x /data/local/tmp/local-recording.sh" +mkdir -p ~/.maestro/tests +# Start logcat in the background and save the output to a file, use `org.matrix.rust.sdk` tag since the SDK handles the logging +adb logcat 'org.matrix.rust.sdk:D *:S' > ~/.maestro/tests/logcat.txt & adb shell "/data/local/tmp/local-recording.sh & echo \$! > /data/local/tmp/screenrecord_pid.txt" & set +e ~/.maestro/bin/maestro test .maestro/allTests.yaml diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index d945b18f5c..40cf9d0058 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -50,7 +50,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Build debug code and test fixtures diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 914bf4b35c..f45a926814 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -22,7 +22,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 602ad1e18e..0ce66df478 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: sudo swapon /mnt/swapfile sudo swapon --show - name: ⏬ Checkout with LFS - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -68,7 +68,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} @@ -108,7 +108,7 @@ jobs: # https://github.com/codecov/codecov-action - name: ☂️ Upload coverage reports to codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml index c3158291c3..027c7d68e9 100644 --- a/.github/workflows/validate-lfs.yml +++ b/.github/workflows/validate-lfs.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest name: Validate steps: - - uses: nschloe/action-cached-lfs-checkout@1c185ad576953eab13e35ffe1bffef437d97e9d2 # v1.2.4 + - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - run: | ./tools/git/validate_lfs.sh diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index f27f5dada3..a276182825 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -2,14 +2,14 @@ appId: ${MAESTRO_APP_ID} --- - tapOn: id: "home_screen-settings" -- tapOn: "Sign out" +- tapOn: "Remove this device" - takeScreenshot: build/maestro/900-SignOutScreen - back -- tapOn: "Sign out" +- tapOn: "Remove this device" # Ensure cancel cancels - tapOn: id: "dialog-negative" -- tapOn: "Sign out" +- tapOn: "Remove this device" - tapOn: id: "dialog-positive" - runFlow: ../assertions/assertInitDisplayed.yaml diff --git a/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml b/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml index fff0fe7b32..d2160e0934 100644 --- a/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml +++ b/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml @@ -1,5 +1,5 @@ appId: ${MAESTRO_APP_ID} --- - extendedWaitUntil: - visible: "Confirm your identity" + visible: "Confirm your digital identity" timeout: 60000 diff --git a/.maestro/tests/roomList/createAndDeleteDM.yaml b/.maestro/tests/roomList/createAndDeleteDM.yaml index e80dc377b5..a6279151ea 100644 --- a/.maestro/tests/roomList/createAndDeleteDM.yaml +++ b/.maestro/tests/roomList/createAndDeleteDM.yaml @@ -1,7 +1,7 @@ appId: ${MAESTRO_APP_ID} --- # Purpose: Test the creation and deletion of a DM room. -- tapOn: "Create a new conversation or room" +- tapOn: "Create room" - tapOn: "Search for someone" - inputText: ${MAESTRO_INVITEE1_MXID} - tapOn: diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml index d0b17133d5..b53066ccd5 100644 --- a/.maestro/tests/roomList/createAndDeleteRoom.yaml +++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml @@ -1,7 +1,7 @@ appId: ${MAESTRO_APP_ID} --- # Purpose: Test the creation and deletion of a room -- tapOn: "Create a new conversation or room" +- tapOn: "Create room" - tapOn: "New room" - tapOn: "Add name…" - inputText: "aRoomName" diff --git a/.maestro/tests/roomList/timeline/messages/location.yaml b/.maestro/tests/roomList/timeline/messages/location.yaml index c9382bd30c..863a699fd9 100644 --- a/.maestro/tests/roomList/timeline/messages/location.yaml +++ b/.maestro/tests/roomList/timeline/messages/location.yaml @@ -2,6 +2,6 @@ appId: ${MAESTRO_APP_ID} --- - takeScreenshot: build/maestro/520-Timeline - tapOn: "Add attachment" -- tapOn: "Location" -- tapOn: "Share my location" +- tapOn: "Share location" +- tapOn: "Share selected location" - takeScreenshot: build/maestro/521-Timeline diff --git a/CHANGES.md b/CHANGES.md index 1b002992fb..68848d491f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,142 @@ +Changes in Element X v26.04.3 +============================= + + + +## What's Changed +### ✨ Features +* Sign in with element classic final by @bmarty in https://github.com/element-hq/element-x-android/pull/6296 +* Take into account homeserver capabilities by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6507 +### 🙌 Improvements +* feat: Default to camera muted when joining ongoing voice call by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6574 +### 🐛 Bugfixes +* Fix crash in FetchPushForegroundService: No super method onTimeout by @bmarty in https://github.com/element-hq/element-x-android/pull/6547 +* Ensure mark as fully read is called only once when leaving the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/6550 +* Fix `isInAirGappedEnvironment` check for older APIs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6573 +* Fix loading initial items of non-live timelines by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6598 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6537 +* Sync Strings - new translations in Japanese and Vietnamese by @ElementBot in https://github.com/element-hq/element-x-android/pull/6568 +### 🧱 Build +* Fix module dependencies by @bmarty in https://github.com/element-hq/element-x-android/pull/6559 +### 🚧 In development 🚧 +* Add confirmation dialog when inviting users with unknown identities by @kaylendog in https://github.com/element-hq/element-x-android/pull/6523 +* Feature: add room threads list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6575 +### Dependency upgrades +* fix(deps): update media3 to v1.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6529 +* fix(deps): update dependency io.github.sergio-sastre.composablepreviewscanner:android to v0.8.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6525 +* fix(deps): update metro to v0.12.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6543 +* fix(deps): update kotlinpoet to v2.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6528 +* Sync compound tokens https://github.com/element-hq/compound-design-tokens/releases/tag/v10.0.0 by @bmarty in https://github.com/element-hq/element-x-android/pull/6517 +* fix(deps): update dependency io.sentry:sentry-android to v8.37.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6508 +* fix(deps): update dependency org.maplibre.gl:android-sdk to v13.0.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6546 +* Update codecov/codecov-action action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6521 +* Update telephoto to v0.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6558 +* Update dependency net.zetetic:sqlcipher-android to v4.14.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6552 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.04.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6553 +* Update gradle/actions action to v6.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6562 +* Update metro to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6565 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.04.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6570 +* Update wysiwyg to v2.41.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6572 +* Update dependency com.google.testparameterinjector:test-parameter-injector to v1.22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6576 +* Use `Coil3` for `ZoomableAsyncImage` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6582 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.04.15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6595 +* Update nschloe/action-cached-lfs-checkout action to v1.2.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6600 +### Others +* Fix portrait image metadata when uploading without media optimization by @kalix127 in https://github.com/element-hq/element-x-android/pull/6362 +* Fix Threads not tappable in pinned messages list by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6535 +* Reduce log level of activity lifecycle from warning to debug. by @bmarty in https://github.com/element-hq/element-x-android/pull/6548 +* Remove spaces features flags by @bmarty in https://github.com/element-hq/element-x-android/pull/6560 +* Remove space announcement by @bmarty in https://github.com/element-hq/element-x-android/pull/6561 +* Update metro to v0.13.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6571 +* Take into account the value of FeatureFlags.SignInWithClassic by @bmarty in https://github.com/element-hq/element-x-android/pull/6586 +* Add extra logs for timeline pagination by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6589 +* Scrollable media caption - tweaks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6583 +* Split developer settings into 2 screens to be able to access global settings when no logged in. by @bmarty in https://github.com/element-hq/element-x-android/pull/6587 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.2...v26.04.3 + +Changes in Element X v26.04.2 +============================= + +## What's Changed +### 🐛 Bugfixes +* Restore enterprise submodule. by @bmarty in https://github.com/element-hq/element-x-android/pull/6541 +### Dependency upgrades +* fix(deps): update dependency io.element.android:element-call-embedded to v0.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6538 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.1...v26.04.2 + +Changes in Element X v26.04.1 +============================= + +## What's Changed +### ✨ Features +* Add support for slash commands (under Feature Flag) by @bmarty in https://github.com/element-hq/element-x-android/pull/6482 +### Dependency upgrades +* chore(deps): update gradle/actions action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6489 +* fix(deps): update dependency androidx.work:work-runtime-ktx to v2.11.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6479 +* fix(deps): update dependency net.zetetic:sqlcipher-android to v4.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6460 +* fix(deps): update metro to v0.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6503 +* fix(deps): update dependency androidx.compose:compose-bom to v2026.03.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6511 +* fix(deps): update dependency org.jetbrains.kotlinx:kover-gradle-plugin to v0.9.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6513 +* fix(deps): update dependency androidx.browser:browser to v1.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6515 +* fix(deps): update dependency io.element.android:emojibase-bindings to v1.5.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6493 +* fix(deps): update core to v1.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6328 +### Others +* Tentative fix for `ForegroundServiceStartNotAllowedException` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6509 +* Fix a missing : in build-rust-sdk by @andybalaam in https://github.com/element-hq/element-x-android/pull/6522 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.0...v26.04.1 + +Changes in Element X v26.04.0 +============================= + +## What's Changed +### ✨ Features +* Add floating/sticky date badge in the timeline by @kalix127 in https://github.com/element-hq/element-x-android/pull/6496 +### 🐛 Bugfixes +* Fix `ForegroundServiceDidNotStartInTimeException` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6470 +* Fix media cover placeholder floating by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6484 +* Try handling `ForegroundServiceStartNotAllowedException` better by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6483 +* Fix crash when using `View.hideKeyboardAndAwaitAnimation` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6502 +* Fix content scrolling not working in the RTE by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6492 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6486 +### 🧱 Build +* Add instructions for AI by @bmarty in https://github.com/element-hq/element-x-android/pull/6468 +* Fix permissions to publish GitHub pages. by @bmarty in https://github.com/element-hq/element-x-android/pull/6500 +* Try fixing location pin previews by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6495 +* CI: yet another Maestro fix by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6505 +### 📄 Documentation +* Add some instructions for features to the community PR notice message by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6465 +### 🚧 In development 🚧 +* Setup live location sharing feature by @ganfra in https://github.com/element-hq/element-x-android/pull/6342 +### Dependency upgrades +* Update dependency io.sentry:sentry-android to v8.36.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6461 +* Update metro to v0.11.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6448 +* Sync compound tokens https://github.com/element-hq/compound-design-tokens/releases/tag/v8.0.0 by @bmarty in https://github.com/element-hq/element-x-android/pull/6459 +* Update sqldelight to v2.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6449 +* Update nschloe/action-cached-lfs-checkout action to v1.2.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6442 +* Update kotlin to v2.3.20 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6437 +* Update dependency io.element.android:element-call-embedded to v0.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6358 +* fix(deps): update dependency io.element.android:emojibase-bindings to v1.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6474 +* fix(deps): update dependency com.google.firebase:firebase-bom to v34.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6478 +* fix(deps): update dependency io.element.android:emojibase-bindings to v1.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6487 +* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.21.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6499 +* fix(deps): update dependency com.posthog:posthog-android to v3.39.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6504 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v26.03.31 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6494 +### Others +* Iterate on space header by @bmarty in https://github.com/element-hq/element-x-android/pull/6456 +* Add margin after bullet points by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6446 +* chore: update the build-rust-sdk script by @bnjbvr in https://github.com/element-hq/element-x-android/pull/6476 +* Update replied message UI by @bmarty in https://github.com/element-hq/element-x-android/pull/6472 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.03.4...v26.04.0 + Changes in Element X v26.03.4 ============================= diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index b522edd137..502518bf3c 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -52,7 +52,7 @@ class MainActivity : NodeActivity() { private lateinit var appBindings: AppBindings override fun onCreate(savedInstanceState: Bundle?) { - Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}") + Timber.tag(loggerTag.value).d("onCreate, with savedInstanceState: ${savedInstanceState != null}") installSplashScreen() super.onCreate(savedInstanceState) appBindings = bindings() @@ -108,7 +108,7 @@ class MainActivity : NodeActivity() { plugins = listOf( object : NodeReadyObserver { override fun init(node: MainNode) { - Timber.tag(loggerTag.value).w("onMainNodeInit") + Timber.tag(loggerTag.value).d("onMainNodeInit") mainNode = node mainNode.handleIntent(intent) } @@ -144,7 +144,7 @@ class MainActivity : NodeActivity() { */ override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - Timber.tag(loggerTag.value).w("onNewIntent") + Timber.tag(loggerTag.value).d("onNewIntent") // If the mainNode is not init yet, keep the intent for later. // It can happen when the activity is killed by the system. The methods are called in this order : // onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit @@ -157,16 +157,16 @@ class MainActivity : NodeActivity() { override fun onPause() { super.onPause() - Timber.tag(loggerTag.value).w("onPause") + Timber.tag(loggerTag.value).d("onPause") } override fun onResume() { super.onResume() - Timber.tag(loggerTag.value).w("onResume") + Timber.tag(loggerTag.value).d("onResume") } override fun onDestroy() { super.onDestroy() - Timber.tag(loggerTag.value).w("onDestroy") + Timber.tag(loggerTag.value).d("onDestroy") } } diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index a171646bad..c95b3a5cc0 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -19,6 +19,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt index d4fe7d1fc5..7e5f064ef4 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt @@ -18,7 +18,6 @@ object TimelineConfig { */ val excludedEvents = listOf( StateEventType.CallMember, - StateEventType.RoomAliases, StateEventType.RoomCanonicalAlias, StateEventType.RoomGuestAccess, StateEventType.RoomHistoryVisibility, diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index ecac391216..24a0355b3f 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation(projects.features.linknewdevice.api) implementation(projects.features.share.api) - implementation(projects.services.apperror.impl) + implementation(projects.services.apperror.api) implementation(projects.services.appnavstate.api) implementation(projects.services.analytics.api) @@ -67,8 +67,7 @@ dependencies { testImplementation(projects.features.messages.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.rageshake.test) - testImplementation(projects.services.appnavstate.impl) + testImplementation(projects.services.apperror.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.analytics.test) - testImplementation(projects.services.toolbox.test) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index a676df1d32..44c1060e10 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -422,6 +422,10 @@ class LoggedInFlowNode( override fun navigateToGlobalNotificationSettings() { backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) } + + override fun navigateToDeveloperSettings() { + backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.DeveloperSettings)) + } } val inputs = RoomFlowNode.Inputs( roomIdOrAlias = navTarget.roomIdOrAlias, @@ -744,11 +748,11 @@ private class AttachRoomOperation( } } + // Always create a new element, otherwise we wouldn't be navigating to the target event id or child node BackStackElement( - key = NavKey(roomTarget), - fromState = CREATED, - targetState = ACTIVE, - operation = this - ) + key = NavKey(roomTarget), + fromState = CREATED, + targetState = ACTIVE, + operation = this + ) } else { // Otherwise, just push the new node to the end of the backstack Push(roomTarget).invoke(currentElements) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 745ab390b2..0e458d3b9c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -252,7 +252,8 @@ class RootFlowNode( val transitionHandler = rememberDelegateTransitionHandler { navTarget -> when (navTarget) { is NavTarget.SplashScreen, - is NavTarget.LoggedInFlow -> backstackFader + is NavTarget.LoggedInFlow, + is NavTarget.NotLoggedInFlow -> backstackFader else -> backstackSlider } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 757dd73395..8dc2de5e4e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -56,6 +58,7 @@ class LoggedInPresenter( private val analyticsService: AnalyticsService, private val encryptionService: EncryptionService, private val buildMeta: BuildMeta, + private val networkMonitor: NetworkMonitor, ) : Presenter { @Composable override fun present(): LoggedInState { @@ -107,6 +110,14 @@ class LoggedInPresenter( }.launchIn(this) } + val networkConnectivity by networkMonitor.connectivity.collectAsState() + LaunchedEffect(networkConnectivity) { + if (networkConnectivity == NetworkStatus.Connected) { + // Refresh homeserver capabilities when the network is back + matrixClient.homeserverCapabilities().refresh() + } + } + fun handleEvent(event: LoggedInEvents) { when (event) { is LoggedInEvents.CloseErrorDialog -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index d0d3df590d..febd15e9c2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -85,6 +85,7 @@ class JoinedRoomLoadedFlowNode( fun navigateToRoom(roomId: RoomId, serverNames: List) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun navigateToGlobalNotificationSettings() + fun navigateToDeveloperSettings() } data class Inputs( @@ -145,6 +146,10 @@ class JoinedRoomLoadedFlowNode( callback.navigateToGlobalNotificationSettings() } + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } + override fun navigateToRoom(roomId: RoomId, serverNames: List) { callback.navigateToRoom(roomId, serverNames) } @@ -252,6 +257,10 @@ class JoinedRoomLoadedFlowNode( override fun navigateToRoom(roomId: RoomId) { callback.navigateToRoom(roomId, emptyList()) } + + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } } val params = MessagesEntryPoint.Params( MessagesEntryPoint.InitialTarget.Messages(navTarget.focusedEventId) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt index 32c8e52084..19aa820433 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt @@ -22,7 +22,7 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionVie import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.services.apperror.impl.AppErrorView +import io.element.android.services.apperror.api.AppErrorView @Composable fun RootView( diff --git a/appnav/src/main/res/values-ja/translations.xml b/appnav/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..3405cc1d68 --- /dev/null +++ b/appnav/src/main/res/values-ja/translations.xml @@ -0,0 +1,6 @@ + + + "ログアウトしてアップグレード" + "%1$s は古いプロトコルに非対応になりました。アプリを引き続き使用するには、ログアウトしてから再度ログインしてください。" + "使用しているホームサーバーは古いプロトコルに非対応になりました。アプリケーションを引き続き使用するには、ログアウトしてから再度ログインしてください。" + diff --git a/appnav/src/main/res/values-vi/translations.xml b/appnav/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..8fa3a7ee30 --- /dev/null +++ b/appnav/src/main/res/values-vi/translations.xml @@ -0,0 +1,6 @@ + + + "Đăng xuất & Nâng cấp" + "%1$s không còn hỗ trợ giao thức cũ. Vui lòng đăng xuất và đăng nhập lại để tiếp tục sử dụng ứng dụng." + "Homeserver của bạn không còn hỗ trợ giao thức cũ. Vui lòng đăng xuất và đăng nhập lại để tiếp tục sử dụng ứng dụng." + diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt index 8d514a2c0f..6dffda9e42 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt @@ -43,7 +43,7 @@ import io.element.android.services.analytics.api.watchers.AnalyticsSendMessageWa import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.watchers.FakeAnalyticsSendMessageWatcher import io.element.android.services.appnavstate.api.ActiveRoomsHolder -import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder +import io.element.android.services.appnavstate.test.FakeActiveRoomsHolder import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -128,7 +128,7 @@ class JoinedRoomLoadedFlowNodeTest { roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(), forwardEntryPoint: ForwardEntryPoint = FakeForwardEntryPoint(), - activeRoomsHolder: ActiveRoomsHolder = DefaultActiveRoomsHolder(), + activeRoomsHolder: ActiveRoomsHolder = FakeActiveRoomsHolder(), matrixClient: FakeMatrixClient = FakeMatrixClient(), ) = JoinedRoomLoadedFlowNode( buildContext = BuildContext.root(savedStateMap = null), @@ -213,7 +213,7 @@ class JoinedRoomLoadedFlowNodeTest { val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) - val activeRoomsHolder = DefaultActiveRoomsHolder() + val activeRoomsHolder = FakeActiveRoomsHolder() val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs, FakeJoinedRoomLoadedFlowNodeCallback()), messagesEntryPoint = fakeMessagesEntryPoint, @@ -236,7 +236,7 @@ class JoinedRoomLoadedFlowNodeTest { val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root()) - val activeRoomsHolder = DefaultActiveRoomsHolder().apply { + val activeRoomsHolder = FakeActiveRoomsHolder().apply { addRoom(room) } val roomFlowNode = createJoinedRoomLoadedFlowNode( diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt index 73d55135fb..9ba98a3f72 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RootPresenterTest.kt @@ -19,8 +19,7 @@ import io.element.android.libraries.matrix.test.FakeSdkMetadata import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.apperror.api.AppErrorState import io.element.android.services.apperror.api.AppErrorStateService -import io.element.android.services.apperror.impl.DefaultAppErrorStateService -import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.apperror.test.FakeAppErrorStateService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -44,10 +43,16 @@ class RootPresenterTest { @Test fun `present - passes app error state`() = runTest { val presenter = createRootPresenter( - appErrorService = DefaultAppErrorStateService( - stringProvider = FakeStringProvider(), - ).apply { - showError("Bad news", "Something bad happened") + appErrorService = FakeAppErrorStateService().apply { + setAppErrorState( + AppErrorState.Error( + title = "Bad news", + body = "Something bad happened", + dismiss = { + setAppErrorState(AppErrorState.NoError) + } + ) + ) } ) moleculeFlow(RecompositionMode.Immediate) { @@ -65,9 +70,7 @@ class RootPresenterTest { } private fun createRootPresenter( - appErrorService: AppErrorStateService = DefaultAppErrorStateService( - stringProvider = FakeStringProvider(), - ), + appErrorService: AppErrorStateService = FakeAppErrorStateService(), ): RootPresenter { return RootPresenter( crashDetectionPresenter = { aCrashDetectionState() }, diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index f1759eab3e..d147a4ed68 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -14,6 +14,8 @@ import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties +import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId @@ -27,6 +29,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService @@ -109,6 +112,7 @@ class LoggedInPresenterTest { val verificationService = FakeSessionVerificationService() val encryptionService = FakeEncryptionService() val buildMeta = aBuildMeta() + val networkMonitor = FakeNetworkMonitor() LoggedInPresenter( matrixClient = FakeMatrixClient( roomListService = roomListService, @@ -122,6 +126,7 @@ class LoggedInPresenterTest { analyticsService = analyticsService, encryptionService = encryptionService, buildMeta = buildMeta, + networkMonitor = networkMonitor, ).test { encryptionService.emitRecoveryState(RecoveryState.UNKNOWN) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) @@ -319,6 +324,27 @@ class LoggedInPresenterTest { } } + @Test + fun `present - refreshes homeserver capabilities when network is back`() = runTest { + val refreshLambda = lambdaRecorder> { Result.success(Unit) } + val matrixClient = FakeMatrixClient( + homeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(refresh = refreshLambda), + accountManagementUrlResult = { Result.success(null) }, + ) + val networkMonitor = FakeNetworkMonitor() + createLoggedInPresenter( + matrixClient = matrixClient, + networkMonitor = networkMonitor, + ).test { + awaitItem() + networkMonitor.connectivity.value = NetworkStatus.Connected + + advanceUntilIdle() + + refreshLambda.assertions().isCalledOnce() + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { skipItems(1) return awaitItem() @@ -334,6 +360,7 @@ class LoggedInPresenterTest { accountManagementUrlResult = { Result.success(null) }, ), buildMeta: BuildMeta = aBuildMeta(), + networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(), ): LoggedInPresenter { return LoggedInPresenter( matrixClient = matrixClient, @@ -343,6 +370,7 @@ class LoggedInPresenterTest { analyticsService = analyticsService, encryptionService = encryptionService, buildMeta = buildMeta, + networkMonitor = networkMonitor, ) } } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt index 40778ae353..2f17071870 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/joined/FakeJoinedRoomLoadedFlowNodeCallback.kt @@ -16,4 +16,5 @@ class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback { override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError() + override fun navigateToDeveloperSettings() = lambdaError() } diff --git a/fastlane/metadata/android/en-US/changelogs/202604000.txt b/fastlane/metadata/android/en-US/changelogs/202604000.txt new file mode 100644 index 0000000000..26b1479a0d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202604000.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes for crashes from the SDK and notifications and UI improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202604010.txt b/fastlane/metadata/android/en-US/changelogs/202604010.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202604010.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/202604020.txt b/fastlane/metadata/android/en-US/changelogs/202604020.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202604020.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/202604030.txt b/fastlane/metadata/android/en-US/changelogs/202604030.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202604030.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file diff --git a/features/analytics/api/src/main/res/values-ja/translations.xml b/features/analytics/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5554ee162f --- /dev/null +++ b/features/analytics/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "問題発見のため、匿名の使用データの共有にご協力ください。" + "利用規約の全文を%1$sから確認することができます。" + "こちら" + "使用データを共有" + diff --git a/features/analytics/api/src/main/res/values-lt/translations.xml b/features/analytics/api/src/main/res/values-lt/translations.xml index 08dd155332..004b20d79d 100644 --- a/features/analytics/api/src/main/res/values-lt/translations.xml +++ b/features/analytics/api/src/main/res/values-lt/translations.xml @@ -1,7 +1,7 @@ - "Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas." + "Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas." "Galite perskaityti visas mūsų sąlygas %1$s." "čia" - "Dalytis analitiniais duomenimis" + "Bendrinti analitinius duomenis" diff --git a/features/analytics/api/src/main/res/values-vi/translations.xml b/features/analytics/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..0dc02d79d6 --- /dev/null +++ b/features/analytics/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề." + "Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s" + "tại đây" + "Chia sẻ dữ liệu phân tích" + diff --git a/features/analytics/impl/src/main/res/values-ja/translations.xml b/features/analytics/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..2cee69962c --- /dev/null +++ b/features/analytics/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,10 @@ + + + "いかなる個人情報も記録, 分析されることはありません" + "問題発見のため、匿名の使用データの共有にご協力ください。" + "利用規約の全文を%1$sから確認することができます。" + "こちら" + "いつでも設定は変更できます" + "情報が第三者に共有されることはありません" + "%1$s の改善にご協力ください" + diff --git a/features/analytics/impl/src/main/res/values-lt/translations.xml b/features/analytics/impl/src/main/res/values-lt/translations.xml index 11f2cbc74e..4f7c70dd6d 100644 --- a/features/analytics/impl/src/main/res/values-lt/translations.xml +++ b/features/analytics/impl/src/main/res/values-lt/translations.xml @@ -1,10 +1,10 @@ - "Mes nekaupsime ir neprofiliuosime jokių asmens duomenų" - "Dalinkitės anoniminiais naudojimo duomenimis ir padėkite mums nustatyti problemas." + "Mes neįrašysime ar neprofiliuosime jokių asmeninių duomenų." + "Bendrinkite anoniminius naudojimo duomenis, kad padėtumėte mums nustatyti problemas." "Galite perskaityti visas mūsų sąlygas %1$s." "čia" - "Tai galite bet kada išjungti" - "Mes nesidalinsime Jūsų duomenimis su trečiosiomis šalimis" - "Padėkite pagerinti %1$s" + "Tai galite išjungti bet kuriuo metu." + "Mes nebendrinsime jūsų duomenų su trečiosiomis šalimis." + "Padėkite patobulinti „%1$s“" diff --git a/features/analytics/impl/src/main/res/values-vi/translations.xml b/features/analytics/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..205a52c296 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,10 @@ + + + "Chúng tôi sẽ không ghi lại hoặc lập hồ sơ bất kỳ dữ liệu cá nhân nào." + "Chia sẻ dữ liệu sử dụng ẩn danh để giúp chúng tôi xác định vấn đề." + "Bạn có thể xem tất cả điều khoản của chúng tôi tại %1$s" + "tại đây" + "Bạn có thể tắt tính năng này bất cứ lúc nào" + "Chúng tôi sẽ không chia sẻ dữ liệu của bạn với bên thứ ba." + "Giúp cải thiện %1$s" + diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt index 0bf35650a0..d743ae4cd6 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/Announcement.kt @@ -8,7 +8,13 @@ package io.element.android.features.announcement.api -enum class Announcement { - Space, - NewNotificationSound, +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface Announcement { + enum class Fullscreen : Announcement { + Space, + } + + data object NewNotificationSound : Announcement } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementEvent.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementEvent.kt new file mode 100644 index 0000000000..947a3ceeba --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementEvent.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl + +import io.element.android.features.announcement.api.Announcement + +sealed interface AnnouncementEvent { + data class Continue( + val announcement: Announcement.Fullscreen, + ) : AnnouncementEvent +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt index 508f1e44a0..bd45ddb956 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenter.kt @@ -12,12 +12,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Inject import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch @Inject class AnnouncementPresenter( @@ -25,13 +29,39 @@ class AnnouncementPresenter( ) : Presenter { @Composable override fun present(): AnnouncementState { - val showSpaceAnnouncement by remember { - announcementStore.announcementStatusFlow(Announcement.Space).map { - it == AnnouncementStatus.Show + val coroutineScope = rememberCoroutineScope() + + val fullscreenAnnouncementToShow by remember { + combine( + flowOf(Unit), + announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).map { + it == AnnouncementStatus.Show + }, + // Add other announcements here when needed + ) { _, showFullscreenSpace -> + when { + showFullscreenSpace -> Announcement.Fullscreen.Space + else -> { + null + } + } } - }.collectAsState(false) + }.collectAsState(null) + + fun handle(event: AnnouncementEvent) { + when (event) { + is AnnouncementEvent.Continue -> coroutineScope.launch { + announcementStore.setAnnouncementStatus( + announcement = event.announcement, + status = AnnouncementStatus.Shown, + ) + } + } + } + return AnnouncementState( - showSpaceAnnouncement = showSpaceAnnouncement, + announcement = fullscreenAnnouncementToShow, + eventSink = ::handle, ) } } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt index e762dd607f..fb0732450d 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementState.kt @@ -8,12 +8,9 @@ package io.element.android.features.announcement.impl -data class AnnouncementState( - val showSpaceAnnouncement: Boolean, -) +import io.element.android.features.announcement.api.Announcement -fun anAnnouncementState( - showSpaceAnnouncement: Boolean = false, -) = AnnouncementState( - showSpaceAnnouncement = showSpaceAnnouncement, +data class AnnouncementState( + val announcement: Announcement.Fullscreen?, + val eventSink: (AnnouncementEvent) -> Unit, ) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt new file mode 100644 index 0000000000..2412fee167 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.announcement.api.Announcement + +open class AnnouncementStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAnnouncementState(), + anAnnouncementState( + announcement = Announcement.Fullscreen.Space, + ), + ) +} + +fun anAnnouncementState( + announcement: Announcement.Fullscreen? = null, + eventSink: (AnnouncementEvent) -> Unit = {}, +) = AnnouncementState( + announcement = announcement, + eventSink = eventSink, +) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt index 0e5c30178c..adb81db61a 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementService.kt @@ -8,35 +8,28 @@ package io.element.android.features.announcement.impl -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementView +import io.element.android.features.announcement.impl.fullscreen.FullscreenAnnouncementView import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore -import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf @ContributesBinding(AppScope::class) class DefaultAnnouncementService( private val announcementStore: AnnouncementStore, - private val announcementPresenter: Presenter, - private val spaceAnnouncementPresenter: Presenter, + private val announcementPresenter: AnnouncementPresenter, ) : AnnouncementService { override suspend fun showAnnouncement(announcement: Announcement) { when (announcement) { - Announcement.Space -> showSpaceAnnouncement() + is Announcement.Fullscreen -> showFullscreenAnnouncement(announcement) Announcement.NewNotificationSound -> { announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show) } @@ -49,13 +42,10 @@ class DefaultAnnouncementService( override fun announcementsToShowFlow(): Flow> { return combine( - announcementStore.announcementStatusFlow(Announcement.Space), + flowOf(Unit), announcementStore.announcementStatusFlow(Announcement.NewNotificationSound), - ) { spaceAnnouncementStatus, newNotificationSoundStatus -> + ) { _, newNotificationSoundStatus -> buildList { - if (spaceAnnouncementStatus == AnnouncementStatus.Show) { - add(Announcement.Space) - } if (newNotificationSoundStatus == AnnouncementStatus.Show) { add(Announcement.NewNotificationSound) } @@ -63,27 +53,19 @@ class DefaultAnnouncementService( } } - private suspend fun showSpaceAnnouncement() { - val currentValue = announcementStore.announcementStatusFlow(Announcement.Space).first() + private suspend fun showFullscreenAnnouncement(announcement: Announcement.Fullscreen) { + val currentValue = announcementStore.announcementStatusFlow(announcement).first() if (currentValue == AnnouncementStatus.NeverShown) { - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show) + announcementStore.setAnnouncementStatus(announcement, AnnouncementStatus.Show) } } @Composable override fun Render(modifier: Modifier) { val announcementState = announcementPresenter.present() - Box(modifier = modifier.fillMaxSize()) { - AnimatedVisibility( - visible = announcementState.showSpaceAnnouncement, - enter = fadeIn(), - exit = fadeOut(), - ) { - val spaceAnnouncementState = spaceAnnouncementPresenter.present() - SpaceAnnouncementView( - state = spaceAnnouncementState, - ) - } - } + FullscreenAnnouncementView( + state = announcementState, + modifier = modifier, + ) } } diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt deleted file mode 100644 index 4cfc073271..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/di/AnnouncementModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.di - -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.Binds -import dev.zacsweers.metro.ContributesTo -import io.element.android.features.announcement.impl.AnnouncementPresenter -import io.element.android.features.announcement.impl.AnnouncementState -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementPresenter -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState -import io.element.android.libraries.architecture.Presenter - -@ContributesTo(AppScope::class) -@BindingContainer -interface AnnouncementModule { - @Binds - fun bindAnnouncementPresenter(presenter: AnnouncementPresenter): Presenter - - @Binds - fun bindSpaceAnnouncementPresenter(presenter: SpaceAnnouncementPresenter): Presenter -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt new file mode 100644 index 0000000000..c544fd4914 --- /dev/null +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.announcement.impl.fullscreen + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.announcement.api.Announcement +import io.element.android.features.announcement.impl.AnnouncementEvent +import io.element.android.features.announcement.impl.AnnouncementState +import io.element.android.features.announcement.impl.AnnouncementStateProvider +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem +import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181 + */ +@Composable +fun FullscreenAnnouncementView( + state: AnnouncementState, + modifier: Modifier = Modifier, +) { + // Ensure that the content stays visible during the exit animation + var fullscreenAnnouncement by remember { mutableStateOf(null) } + if (state.announcement != null) { + fullscreenAnnouncement = state.announcement + } + Box(modifier = modifier.fillMaxSize()) { + AnimatedVisibility( + visible = state.announcement != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + fullscreenAnnouncement?.let { + FullscreenAnnouncementView( + announcement = it, + eventSink = state.eventSink, + ) + } + } + } +} + +@Composable +private fun FullscreenAnnouncementView( + announcement: Announcement.Fullscreen, + eventSink: (AnnouncementEvent) -> Unit, + modifier: Modifier = Modifier +) { + fun onContinue() { + eventSink(AnnouncementEvent.Continue(announcement)) + } + + BackHandler(onBack = ::onContinue) + HeaderFooterPage( + modifier = modifier, + isScrollable = true, + contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp), + header = { + FullscreenAnnouncementHeader(announcement) + }, + content = { + FullscreenAnnouncementContent( + modifier = Modifier.padding(horizontal = 8.dp), + announcement = announcement, + ) + }, + footer = { + FullscreenAnnouncementFooter( + onContinue = ::onContinue, + ) + } + ) +} + +@Composable +private fun FullscreenAnnouncementHeader( + announcement: Announcement.Fullscreen, + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 16.dp, bottom = 16.dp), + title = announcement.title(), + showBetaLabel = true, + subTitle = announcement.subtitle(), + iconStyle = BigIcon.Style.Default( + vectorIcon = announcement.icon(), + usePrimaryTint = true, + ), + ) +} + +@Composable +private fun FullscreenAnnouncementContent( + announcement: Announcement.Fullscreen, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + InfoListOrganism( + modifier = Modifier.fillMaxWidth(), + items = announcement.items(), + textStyle = ElementTheme.typography.fontBodyLgMedium, + iconTint = ElementTheme.colors.iconSecondary, + iconSize = 24.dp + ) + announcement.notice()?.let { notice -> + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + text = notice, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun FullscreenAnnouncementFooter( + onContinue: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 8.dp) + ) { + Button( + text = stringResource(id = CommonStrings.action_continue), + onClick = onContinue, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun Announcement.Fullscreen.title() = when (this) { + Announcement.Fullscreen.Space -> "Introducing Spaces" +} + +@Composable +private fun Announcement.Fullscreen.subtitle() = when (this) { + Announcement.Fullscreen.Space -> "Welcome to the beta version of Spaces! With this first version you can:" +} + +@Composable +private fun Announcement.Fullscreen.icon() = when (this) { + Announcement.Fullscreen.Space -> CompoundIcons.SpaceSolid() +} + +@Composable +private fun Announcement.Fullscreen.items(): ImmutableList = when (this) { + Announcement.Fullscreen.Space -> persistentListOf( + InfoListItem( + message = "View spaces you\'ve created or joined", + iconVector = CompoundIcons.VisibilityOn(), + ), + InfoListItem( + message = "Accept or decline invites to spaces", + iconVector = CompoundIcons.Email(), + ), + InfoListItem( + message = "Discover any rooms you can join in your spaces", + iconVector = CompoundIcons.Search(), + ), + InfoListItem( + message = "Join public spaces", + iconVector = CompoundIcons.Explore(), + ), + InfoListItem( + message = "Leave any spaces you’ve joined", + iconVector = CompoundIcons.Leave(), + ), + ) +} + +@Composable +private fun Announcement.Fullscreen.notice(): String? = when (this) { + Announcement.Fullscreen.Space -> "Filtering, creating and managing spaces is coming soon." +} + +@PreviewsDayNight +@Composable +internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview { + FullscreenAnnouncementView( + state = state, + ) +} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt deleted file mode 100644 index 3b968d09a6..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementEvents.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -sealed interface SpaceAnnouncementEvents { - data object Continue : SpaceAnnouncementEvents -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt deleted file mode 100644 index 7c4bc7b5eb..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenter.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import dev.zacsweers.metro.Inject -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.impl.store.AnnouncementStatus -import io.element.android.features.announcement.impl.store.AnnouncementStore -import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.launch - -@Inject -class SpaceAnnouncementPresenter( - private val announcementStore: AnnouncementStore, -) : Presenter { - @Composable - override fun present(): SpaceAnnouncementState { - val localCoroutineScope = rememberCoroutineScope() - - fun handleEvent(event: SpaceAnnouncementEvents) { - when (event) { - SpaceAnnouncementEvents.Continue -> localCoroutineScope.launch { - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown) - } - } - } - - return SpaceAnnouncementState( - eventSink = ::handleEvent, - ) - } -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt deleted file mode 100644 index 9407fad872..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementState.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -data class SpaceAnnouncementState( - val eventSink: (SpaceAnnouncementEvents) -> Unit -) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt deleted file mode 100644 index 27f48cc7ed..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementStateProvider.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class SpaceAnnouncementStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aSpaceAnnouncementState(), - ) -} - -fun aSpaceAnnouncementState( - eventSink: (SpaceAnnouncementEvents) -> Unit = {}, -) = SpaceAnnouncementState( - eventSink = eventSink, -) diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt deleted file mode 100644 index 3fe6ec4456..0000000000 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementView.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.announcement.impl.R -import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem -import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism -import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.collections.immutable.persistentListOf - -/** - * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4593-40181 - */ -@Composable -fun SpaceAnnouncementView( - state: SpaceAnnouncementState, - modifier: Modifier = Modifier, -) { - val eventSink = state.eventSink - - fun onContinue() { - eventSink(SpaceAnnouncementEvents.Continue) - } - - BackHandler(onBack = ::onContinue) - HeaderFooterPage( - modifier = modifier, - isScrollable = true, - contentPadding = PaddingValues(top = 24.dp, start = 16.dp, end = 16.dp, bottom = 24.dp), - header = { - SpaceAnnouncementHeader() - }, - content = { - SpaceAnnouncementContent( - modifier = Modifier.padding(horizontal = 8.dp), - ) - }, - footer = { - SpaceAnnouncementFooter( - onContinue = ::onContinue, - ) - } - ) -} - -@Composable -private fun SpaceAnnouncementHeader( - modifier: Modifier = Modifier, -) { - IconTitleSubtitleMolecule( - modifier = modifier.padding(top = 16.dp, bottom = 16.dp), - title = stringResource(id = R.string.screen_space_announcement_title), - showBetaLabel = true, - subTitle = stringResource(id = R.string.screen_space_announcement_subtitle), - iconStyle = BigIcon.Style.Default( - vectorIcon = CompoundIcons.SpaceSolid(), - usePrimaryTint = true, - ), - ) -} - -@Composable -private fun SpaceAnnouncementContent( - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier.fillMaxSize(), - ) { - InfoListOrganism( - modifier = Modifier.fillMaxWidth(), - items = persistentListOf( - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item1), - iconVector = CompoundIcons.VisibilityOn(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item2), - iconVector = CompoundIcons.Email(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item3), - iconVector = CompoundIcons.Search(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item4), - iconVector = CompoundIcons.Explore(), - ), - InfoListItem( - message = stringResource(id = R.string.screen_space_announcement_item5), - iconVector = CompoundIcons.Leave(), - ), - ), - textStyle = ElementTheme.typography.fontBodyLgMedium, - iconTint = ElementTheme.colors.iconSecondary, - iconSize = 24.dp - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - text = stringResource(id = R.string.screen_space_announcement_notice), - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun SpaceAnnouncementFooter( - onContinue: () -> Unit, -) { - ButtonColumnMolecule( - modifier = Modifier.padding(bottom = 8.dp) - ) { - Button( - text = stringResource(id = CommonStrings.action_continue), - onClick = onContinue, - modifier = Modifier.fillMaxWidth(), - ) - } -} - -@PreviewsDayNight -@Composable -internal fun SpaceAnnouncementViewPreview(@PreviewParameter(SpaceAnnouncementStateProvider::class) state: SpaceAnnouncementState) = ElementPreview { - SpaceAnnouncementView( - state = state, - ) -} diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt index ad166e4ef5..5a6c135247 100644 --- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt +++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/store/DefaultAnnouncementStore.kt @@ -35,9 +35,10 @@ class DefaultAnnouncementStore( override fun announcementStatusFlow(announcement: Announcement): Flow { val key = announcement.toKey() + // Announcement.Fullscreen.Space is disabled, consider it's shown // For NewNotificationSound, a migration will set it to Show on application upgrade (see AppMigration08) val defaultStatus = when (announcement) { - Announcement.Space -> AnnouncementStatus.NeverShown + Announcement.Fullscreen.Space -> AnnouncementStatus.Shown Announcement.NewNotificationSound -> AnnouncementStatus.Shown } return store.data.map { prefs -> @@ -52,6 +53,6 @@ class DefaultAnnouncementStore( } private fun Announcement.toKey() = when (this) { - Announcement.Space -> spaceAnnouncementKey + Announcement.Fullscreen.Space -> spaceAnnouncementKey Announcement.NewNotificationSound -> newNotificationSoundKey } diff --git a/features/announcement/impl/src/main/res/values-ja/translations.xml b/features/announcement/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..47273cc12d --- /dev/null +++ b/features/announcement/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "作成または参加したスペースを表示できます" + "スペースへの招待を受諾または拒否できます" + "スペース内の参加可能なルームを検索できます" + "公開スペースに参加できます" + "参加したスペースを退出できます" + "スペースの作成や管理, フィルター検索は近日実装予定です。" + "ベータ版のスペースにようこそ。この最新のバージョンでは:" + "スペースの紹介" + diff --git a/features/announcement/impl/src/main/res/values-zh/translations.xml b/features/announcement/impl/src/main/res/values-zh/translations.xml index e01e63b2ae..70d86638ea 100644 --- a/features/announcement/impl/src/main/res/values-zh/translations.xml +++ b/features/announcement/impl/src/main/res/values-zh/translations.xml @@ -6,6 +6,6 @@ "加入公共空间" "离开你加入的所有空间" "筛选、创建及管理空间功能即将上线。" - "欢迎使用 Spaces 测试版!使用首个版本,您可以:" - "Spaces 简介" + "欢迎使用空间测试版!使用首个版本,您可以:" + "空间简介" diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt index 18deb8b2fd..c37f49fad8 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/AnnouncementPresenterTest.kt @@ -14,6 +14,7 @@ import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test @@ -23,25 +24,47 @@ class AnnouncementPresenterTest { val presenter = createAnnouncementPresenter() presenter.test { val state = awaitItem() - assertThat(state.showSpaceAnnouncement).isFalse() + assertThat(state.announcement).isNull() } } @Test - fun `present - showSpaceAnnouncement value depends on the value in the store`() = runTest { + fun `present - showFullscreen value depends on the value in the store`() = runTest { val store = InMemoryAnnouncementStore() val presenter = createAnnouncementPresenter( announcementStore = store, ) presenter.test { val state = awaitItem() - assertThat(state.showSpaceAnnouncement).isFalse() - store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show) + assertThat(state.announcement).isNull() + store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show) val updatedState = awaitItem() - assertThat(updatedState.showSpaceAnnouncement).isTrue() - store.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown) + assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space) + store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Shown) val finalState = awaitItem() - assertThat(finalState.showSpaceAnnouncement).isFalse() + assertThat(finalState.announcement).isNull() + } + } + + @Test + fun `present - continue event will mark the announcement as Shown`() = runTest { + val store = InMemoryAnnouncementStore() + val presenter = createAnnouncementPresenter( + announcementStore = store, + ) + presenter.test { + val state = awaitItem() + assertThat(state.announcement).isNull() + store.setAnnouncementStatus(Announcement.Fullscreen.Space, AnnouncementStatus.Show) + val statusShow = store.announcementStatusFlow(Announcement.Fullscreen.Space).first() + assertThat(statusShow).isEqualTo(AnnouncementStatus.Show) + val updatedState = awaitItem() + assertThat(updatedState.announcement).isEqualTo(Announcement.Fullscreen.Space) + updatedState.eventSink(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) + val statusShown = store.announcementStatusFlow(Announcement.Fullscreen.Space).first() + assertThat(statusShown).isEqualTo(AnnouncementStatus.Shown) + val finalState = awaitItem() + assertThat(finalState.announcement).isNull() } } } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt index e16619129c..c72d147c1d 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/DefaultAnnouncementServiceTest.kt @@ -11,31 +11,28 @@ package io.element.android.features.announcement.impl import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.impl.spaces.SpaceAnnouncementState -import io.element.android.features.announcement.impl.spaces.aSpaceAnnouncementState import io.element.android.features.announcement.impl.store.AnnouncementStatus import io.element.android.features.announcement.impl.store.AnnouncementStore import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore -import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test class DefaultAnnouncementServiceTest { @Test - fun `when showing Space announcement, space announcement is set to show only if it was never shown`() = runTest { + fun `when showing Fullscreen announcement, Fullscreen announcement is set to show only if it was never shown`() = runTest { val announcementStore = InMemoryAnnouncementStore() val sut = createDefaultAnnouncementService( announcementStore = announcementStore, ) - assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown) - sut.showAnnouncement(Announcement.Space) - assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Show) + assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.NeverShown) + sut.showAnnouncement(Announcement.Fullscreen.Space) + assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Show) // Simulate user close the announcement - sut.onAnnouncementDismissed(Announcement.Space) + sut.onAnnouncementDismissed(Announcement.Fullscreen.Space) // Entering again the space tab should not change the value - sut.showAnnouncement(Announcement.Space) - assertThat(announcementStore.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown) + sut.showAnnouncement(Announcement.Fullscreen.Space) + assertThat(announcementStore.announcementStatusFlow(Announcement.Fullscreen.Space).first()).isEqualTo(AnnouncementStatus.Shown) } @Test @@ -62,11 +59,7 @@ class DefaultAnnouncementServiceTest { ) sut.announcementsToShowFlow().test { assertThat(awaitItem()).isEmpty() - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Show) - assertThat(awaitItem()).containsExactly(Announcement.Space) announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Show) - assertThat(awaitItem()).containsExactly(Announcement.Space, Announcement.NewNotificationSound) - announcementStore.setAnnouncementStatus(Announcement.Space, AnnouncementStatus.Shown) assertThat(awaitItem()).containsExactly(Announcement.NewNotificationSound) announcementStore.setAnnouncementStatus(Announcement.NewNotificationSound, AnnouncementStatus.Shown) assertThat(awaitItem()).isEmpty() @@ -75,11 +68,9 @@ class DefaultAnnouncementServiceTest { private fun createDefaultAnnouncementService( announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), - announcementPresenter: Presenter = Presenter { anAnnouncementState() }, - spaceAnnouncementPresenter: Presenter = Presenter { aSpaceAnnouncementState() }, + announcementPresenter: AnnouncementPresenter = AnnouncementPresenter(announcementStore), ) = DefaultAnnouncementService( announcementStore = announcementStore, announcementPresenter = announcementPresenter, - spaceAnnouncementPresenter = spaceAnnouncementPresenter, ) } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt similarity index 51% rename from features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt rename to features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt index ad3d83f1b5..b69037e61a 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementViewTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt @@ -6,12 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.announcement.impl.spaces +package io.element.android.features.announcement.impl.fullscreen import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.announcement.api.Announcement +import io.element.android.features.announcement.impl.AnnouncementEvent +import io.element.android.features.announcement.impl.AnnouncementState +import io.element.android.features.announcement.impl.anAnnouncementState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -22,39 +26,41 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class SpaceAnnouncementViewTest { +class FullscreenAnnouncementViewTest { @get:Rule val rule = createAndroidComposeRule() @Test - fun `clicking on back sends a SpaceAnnouncementEvents`() { - val eventsRecorder = EventsRecorder() - rule.setSpaceAnnouncementView( - aSpaceAnnouncementState( + fun `clicking on back sends a AnnouncementEvent`() { + val eventsRecorder = EventsRecorder() + rule.setFullscreenAnnouncementView( + anAnnouncementState( + announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) rule.pressBackKey() - eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue) + eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } @Test - fun `clicking on Continue sends a SpaceAnnouncementEvents`() { - val eventsRecorder = EventsRecorder() - rule.setSpaceAnnouncementView( - aSpaceAnnouncementState( + fun `clicking on Continue sends a AnnouncementEvent`() { + val eventsRecorder = EventsRecorder() + rule.setFullscreenAnnouncementView( + anAnnouncementState( + announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) rule.clickOn(CommonStrings.action_continue) - eventsRecorder.assertSingle(SpaceAnnouncementEvents.Continue) + eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } } -private fun AndroidComposeTestRule.setSpaceAnnouncementView( - state: SpaceAnnouncementState, +private fun AndroidComposeTestRule.setFullscreenAnnouncementView( + state: AnnouncementState, ) { setContent { - SpaceAnnouncementView( + FullscreenAnnouncementView( state = state, ) } diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt deleted file mode 100644 index 672f677407..0000000000 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/spaces/SpaceAnnouncementPresenterTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.announcement.impl.spaces - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.impl.store.AnnouncementStatus -import io.element.android.features.announcement.impl.store.AnnouncementStore -import io.element.android.features.announcement.impl.store.InMemoryAnnouncementStore -import io.element.android.tests.testutils.test -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class SpaceAnnouncementPresenterTest { - @Test - fun `present - when user continues, the store is updated`() = runTest { - val store = InMemoryAnnouncementStore() - val presenter = createSpaceAnnouncementPresenter( - announcementStore = store, - ) - presenter.test { - assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.NeverShown) - val state = awaitItem() - state.eventSink(SpaceAnnouncementEvents.Continue) - assertThat(store.announcementStatusFlow(Announcement.Space).first()).isEqualTo(AnnouncementStatus.Shown) - } - } -} - -private fun createSpaceAnnouncementPresenter( - announcementStore: AnnouncementStore = InMemoryAnnouncementStore(), -) = SpaceAnnouncementPresenter( - announcementStore = announcementStore, -) diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt index ab3e85124f..ed6dfec850 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/store/InMemoryAnnouncementStore.kt @@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow class InMemoryAnnouncementStore( - initialSpaceAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, + initialFullscreenAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, initialNewNotificationSoundAnnouncementStatus: AnnouncementStatus = AnnouncementStatus.NeverShown, ) : AnnouncementStore { - private val spaceAnnouncement = MutableStateFlow(initialSpaceAnnouncementStatus) + private val fullScreenAnnouncement = MutableStateFlow(initialFullscreenAnnouncementStatus) private val newNotificationSoundAnnouncement = MutableStateFlow(initialNewNotificationSoundAnnouncementStatus) override suspend fun setAnnouncementStatus(announcement: Announcement, status: AnnouncementStatus) { @@ -29,12 +29,12 @@ class InMemoryAnnouncementStore( } override suspend fun reset() { - spaceAnnouncement.value = AnnouncementStatus.NeverShown + fullScreenAnnouncement.value = AnnouncementStatus.NeverShown newNotificationSoundAnnouncement.value = AnnouncementStatus.NeverShown } private fun Announcement.toMutableStateFlow() = when (this) { - Announcement.Space -> spaceAnnouncement + is Announcement.Fullscreen -> fullScreenAnnouncement Announcement.NewNotificationSound -> newNotificationSoundAnnouncement } } diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index e77c09e19a..2843e47870 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -73,7 +73,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) - implementation(projects.libraries.matrix.impl) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) diff --git a/features/call/impl/src/main/res/values-ja/translations.xml b/features/call/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..18ee195d0f --- /dev/null +++ b/features/call/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + "通話中" + "タップして通話に戻る" + "☎️ 通話中" + "Element CallはこのAndroidバージョンにおいて、Bluetoothオーディオデバイスの使用をサポートしていません。別のオーディオデバイスを選択してください。" + "Element Call の着信" + diff --git a/features/call/impl/src/main/res/values-vi/translations.xml b/features/call/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c88cf651e6 --- /dev/null +++ b/features/call/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,8 @@ + + + "Cuộc gọi đang diễn ra" + "Nhấn để quay lại cuộc gọi." + "☎️ Cuộc gọi đang diễn ra" + "Ứng dụng Element Call không hỗ trợ sử dụng thiết bị âm thanh Bluetooth trên phiên bản Android này. Vui lòng chọn thiết bị âm thanh khác." + "Cuộc gọi Element đến" + diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt similarity index 93% rename from features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt rename to features/call/impl/src/test/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt index 387a77ef8b..1906aba551 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/CallNotificationData.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt @@ -6,9 +6,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.call.test +package io.element.android.features.call.impl.notifications -import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index 5650eaa47f..f9f6206ec7 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -15,11 +15,11 @@ import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.features.call.api.CallType import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator +import io.element.android.features.call.impl.notifications.aCallNotificationData import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.DefaultActiveCallManager import io.element.android.features.call.impl.utils.DefaultCurrentCallService -import io.element.android.features.call.test.aCallNotificationData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId diff --git a/features/call/test/build.gradle.kts b/features/call/test/build.gradle.kts index 76fbf9915e..f06c5ed16e 100644 --- a/features/call/test/build.gradle.kts +++ b/features/call/test/build.gradle.kts @@ -20,7 +20,6 @@ dependencies { implementation(projects.libraries.core) api(projects.features.call.api) - implementation(projects.features.call.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.test) implementation(projects.tests.testutils) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 7201f7dc9c..13a195e940 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.previewutils) - implementation(projects.libraries.usersearch.impl) implementation(projects.services.analytics.api) implementation(libs.coil.compose) implementation(projects.libraries.featureflag.api) @@ -52,7 +51,6 @@ dependencies { testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.permissions.test) - testImplementation(projects.libraries.usersearch.test) testImplementation(projects.features.startchat.test) testImplementation(projects.libraries.featureflag.test) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 38d6132e14..0d74ca05bf 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -84,7 +84,6 @@ class ConfigureRoomPresenter( @Composable override fun present(): ConfigureRoomState { - val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val cameraPermissionState = cameraPermissionPresenter.present() val createRoomConfig by dataStore.getCreateRoomConfigFlow().collectAsState() val homeserverName = remember { matrixClient.userIdServerName() } @@ -113,12 +112,8 @@ class ConfigureRoomPresenter( } var spaces by remember { mutableStateOf>(persistentListOf()) } - LaunchedEffect(canAddRoomToSpace) { - spaces = if (canAddRoomToSpace) { - matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList() - } else { - persistentListOf() - } + LaunchedEffect(Unit) { + spaces = matrixClient.spaceService.editableSpaces().getOrElse { emptyList() }.toImmutableList() val parentSpace = spaces.find { it.roomId == initialParentSpaceId } parentSpace?.let { dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true) diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index ce8bca20e0..7a936f41a8 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -3,14 +3,34 @@ "Nuova stanza" "Invita persone" "Si è verificato un errore durante la creazione della stanza" - "Solo le persone invitate possono accedere a questa stanza. Tutti i messaggi sono cifrati end-to-end." + "Non è stato possibile creare lo spazio a causa di un errore sconosciuto. Riprova più tardi." + "Aggiungi nome…" + "Nuova stanza" + "Nuovo spazio" + "Possono partecipare solo le persone invitate." + "Privato" "Chiunque può trovare questa stanza. Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza." - "Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta" - "Chiedi di entrare" - "Chiunque può entrare in questa stanza" - "Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza." - "Indirizzo della stanza" + "Chiunque può partecipare." + "Pubblico" + "Chiunque può chiedere di partecipare, ma un amministratore o un moderatore deve accettare la richiesta." + "Consenti di chiedere di partecipare" + "Chiunque sia membro di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso." + "Richiedi accesso" + "Possono partecipare solo le persone invitate." + "Privato" + "Chiunque può partecipare." + "Pubblico" + "Chiunque in %1$s può unirsi." + "Standard" + "Chi ha accesso" + "Avrai bisogno di un indirizzo per renderlo visibile nella directory pubblica." + "Indirizzo" "Visibilità della stanza" + "(nessuno spazio)" + "Non aggiungere a uno spazio" + "Nessuno spazio selezionato" + "Aggiungi allo spazio" "Argomento (facoltativo)" + "Aggiungi descrizione…" diff --git a/features/createroom/impl/src/main/res/values-ja/translations.xml b/features/createroom/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f6f7c71390 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,36 @@ + + + "新しいルーム" + "ユーザーを招待" + "ルームの作成中に問題が発生しました" + "不明な問題のためスペースを作成できませんでした。再度お試しください。" + "名前を追加…" + "新しいルーム" + "新しいスペース" + "招待されたユーザーのみ参加できます。" + "非公開" + "ルームは全世界に公開されます。 +ルーム設定でいつでも変更できます。" + "誰でも参加できます。" + "公開" + "誰でも参加できますが、管理者またはモデレーターの承認が必要です。" + "参加の要求を許可" + "%1$s にいる全員が参加することができますが、事前に参加の要求をする必要があります。" + "参加を要求" + "招待されたユーザーのみが参加できます。" + "非公開" + "誰でも参加できます。" + "公開" + "%1$s にいる全員が参加することができます。" + "スタンダード" + "参加できるユーザー" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレス" + "ルームの公開度" + "(スペースなし)" + "スペースに追加しない" + "スペースが選択されていません" + "スペースに追加" + "トピック (任意)" + "説明を追加…" + diff --git a/features/createroom/impl/src/main/res/values-vi/translations.xml b/features/createroom/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b10d15e077 --- /dev/null +++ b/features/createroom/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,11 @@ + + + "Phòng mới" + "Mời ai đó" + "Đã xảy ra lỗi khi tạo phòng." + "Chỉ những người được mời mới có thể tham gia." + "Bất kỳ ai cũng có thể tìm thấy phòng này. +Bạn có thể thay đổi cài đặt phòng bất cứ lúc nào." + "Chủ đề (tùy chọn)" + "Thêm mô tả…" + diff --git a/features/deactivation/impl/src/main/res/values-ja/translations.xml b/features/deactivation/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..873b1308ec --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,14 @@ + + + "アカウントを無効化することを再度確認します。この操作は元に戻せません。" + "メッセージをすべて削除" + "注意: 新しいユーザーには断片的な会話が表示されます" + "アカウントを無効化することは %1$s であり、次の変化が生じます:" + "不可逆" + "アカウントを %1$s (再度ログイン不可, 同一のIDを再利用不可)" + "恒久的に無効化する" + "すべてのチャットルームから退出します。" + "アカウント提供元サーバーからアカウント情報を削除します。" + "あなたの会話は、既存ユーザーには引き続き表示されますが、新規ユーザーには表示されなくなります。" + "アカウントを無効化" + diff --git a/features/deactivation/impl/src/main/res/values-vi/translations.xml b/features/deactivation/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..22bc0a6d6e --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Xóa tất cả tin nhắn của tôi" + "Cảnh báo: Người dùng sau này có thể thấy các cuộc trò chuyện chưa hoàn chỉnh." + "Tin nhắn của bạn vẫn sẽ hiển thị cho người dùng đã đăng ký nhưng sẽ không hiển thị cho người dùng mới hoặc chưa đăng ký nếu bạn chọn xóa chúng." + "Vô hiệu hóa tài khoản" + diff --git a/features/ftue/impl/src/main/res/values-cs/translations.xml b/features/ftue/impl/src/main/res/values-cs/translations.xml index 30d6998ac6..0937e7ed14 100644 --- a/features/ftue/impl/src/main/res/values-cs/translations.xml +++ b/features/ftue/impl/src/main/res/values-cs/translations.xml @@ -2,8 +2,8 @@ "Nemůžete potvrdit?" "Vytvoření nového klíče pro obnovení" - "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." - "Potvrďte, že jste to vy" + "Vyberte způsob ověření pro nastavení zabezpečeného zasílání zpráv." + "Potvrďte svou digitální identitu" "Použít jiné zařízení" "Použít klíč pro obnovení" "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." diff --git a/features/ftue/impl/src/main/res/values-da/translations.xml b/features/ftue/impl/src/main/res/values-da/translations.xml index 9c3720ac00..473c8a9c73 100644 --- a/features/ftue/impl/src/main/res/values-da/translations.xml +++ b/features/ftue/impl/src/main/res/values-da/translations.xml @@ -2,8 +2,8 @@ "Kan ikke bekræfte?" "Opret en ny gendannelsesnøgle" - "Verificér denne enhed for at konfigurere sikre meddelelser." - "Bekræft din identitet" + "Vælg, hvordan du vil verificere dig for at konfigurere sikre beskeder." + "Bekræft din digitale identitet" "Brug en anden enhed" "Brug gendannelsesnøgle" "Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed." diff --git a/features/ftue/impl/src/main/res/values-it/translations.xml b/features/ftue/impl/src/main/res/values-it/translations.xml index 25ba544d00..8aa9132732 100644 --- a/features/ftue/impl/src/main/res/values-it/translations.xml +++ b/features/ftue/impl/src/main/res/values-it/translations.xml @@ -2,8 +2,8 @@ "Non puoi confermare?" "Crea una nuova chiave di recupero" - "Verifica questo dispositivo per segnare i tuoi messaggi come sicuri." - "Conferma la tua identità" + "Scegli come effettuare la verifica per configurare la messaggistica sicura." + "Conferma la tua identità digitale" "Usa un altro dispositivo" "Usa la chiave di recupero" "Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo." diff --git a/features/ftue/impl/src/main/res/values-ja/translations.xml b/features/ftue/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..68b69079ef --- /dev/null +++ b/features/ftue/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,15 @@ + + + "認証できませんか?" + "回復鍵を新規作成します" + "安全なメッセージを設定するための検証方法を選択してください。" + "デジタルIDの認証" + "他の端末を使用" + "回復鍵を使用" + "メッセージのやり取りを安全に行えるようになりました。他のユーザーはこの端末を信頼できます。" + "検証済みの端末" + "他の端末を使用" + "一方の端末を待機中…" + "設定は後で変更することができます。" + "メッセージを見逃さないため通知を許可" + diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml index b2f0925813..34ac77ae3f 100644 --- a/features/ftue/impl/src/main/res/values-ru/translations.xml +++ b/features/ftue/impl/src/main/res/values-ru/translations.xml @@ -2,7 +2,7 @@ "Не можете подтвердить?" "Создайте новый ключ восстановления" - "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." + "Выберите способ подтверждения для настройки защищенного обмена сообщениями." "Подтвердите личность" "Использовать другое устройство" "Использовать ключ восстановления" diff --git a/features/ftue/impl/src/main/res/values-vi/translations.xml b/features/ftue/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c70d9be0fb --- /dev/null +++ b/features/ftue/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,10 @@ + + + "Chọn phương thức xác minh để bật nhắn tin bảo mật." + "Xác nhận danh tính kỹ thuật số của bạn" + "Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này." + "Thiết bị được xác thực" + "Đang chờ trên thiết bị khác…" + "Bạn có thể thay đổi cài đặt sau." + "Cho phép thông báo để không bỏ lỡ bất kỳ tin nhắn nào" + diff --git a/features/ftue/impl/src/main/res/values-zh/translations.xml b/features/ftue/impl/src/main/res/values-zh/translations.xml index 669a316243..68a48831e0 100644 --- a/features/ftue/impl/src/main/res/values-zh/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh/translations.xml @@ -2,8 +2,8 @@ "无法确认?" "创建新的恢复密钥" - "验证此设备以开始安全地收发消息。" - "确认这是你" + "选择验证方式以设置安全的消息传输。" + "确认您的数字身份" "使用其他设备" "使用恢复密钥" "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index 6878f4d53c..1e49b1aa70 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -19,8 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState @@ -47,7 +45,6 @@ class HomePresenter( private val logoutPresenter: Presenter, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val sessionStore: SessionStore, - private val announcementService: AnnouncementService, ) : Presenter { private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder() @@ -82,10 +79,7 @@ class HomePresenter( fun handleEvent(event: HomeEvent) { when (event) { - is HomeEvent.SelectHomeNavigationBarItem -> coroutineState.launch { - if (event.item == HomeNavigationBarItem.Spaces) { - announcementService.showAnnouncement(Announcement.Space) - } + is HomeEvent.SelectHomeNavigationBarItem -> { currentHomeNavigationBarItemOrdinal = event.item.ordinal } is HomeEvent.SwitchToAccount -> coroutineState.launch { @@ -94,12 +88,6 @@ class HomePresenter( } } - LaunchedEffect(homeSpacesState.canCreateSpaces, homeSpacesState.spaceRooms.isEmpty()) { - // If the flag to create spaces is disabled and the last space is left, ensure that the Chat view is rendered. - if (!homeSpacesState.canCreateSpaces && homeSpacesState.spaceRooms.isEmpty()) { - currentHomeNavigationBarItemOrdinal = HomeNavigationBarItem.Chats.ordinal - } - } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() return HomeState( currentUserAndNeighbors = currentUserAndNeighbors, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index 934dac831e..ae59ef8eb9 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -34,5 +34,4 @@ data class HomeState( ) { val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters - val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index ddf8b1c499..6956ba6ba5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -199,50 +199,41 @@ private fun HomeScaffold( ) }, floatingActionButton = { - if (state.showNavigationBar) { - val coroutineScope = rememberCoroutineScope() - HomeBottomBar( - currentHomeNavigationBarItem = state.currentHomeNavigationBarItem, - onItemClick = { item -> - // scroll to top if selecting the same item - if (item == state.currentHomeNavigationBarItem) { - val lazyListStateTarget = when (item) { - HomeNavigationBarItem.Chats -> roomsLazyListState - HomeNavigationBarItem.Spaces -> spacesLazyListState - } - coroutineScope.launch { - if (lazyListStateTarget.firstVisibleItemIndex > 10) { - lazyListStateTarget.scrollToItem(10) - } - // Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls - scrollBehavior.state.heightOffset = 0f - lazyListStateTarget.animateScrollToItem(0) - } - } else { - state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item)) + val coroutineScope = rememberCoroutineScope() + HomeBottomBar( + currentHomeNavigationBarItem = state.currentHomeNavigationBarItem, + onItemClick = { item -> + // scroll to top if selecting the same item + if (item == state.currentHomeNavigationBarItem) { + val lazyListStateTarget = when (item) { + HomeNavigationBarItem.Chats -> roomsLazyListState + HomeNavigationBarItem.Spaces -> spacesLazyListState } - }, - floatingActionButton = when (state.currentHomeNavigationBarItem) { + coroutineScope.launch { + if (lazyListStateTarget.firstVisibleItemIndex > 10) { + lazyListStateTarget.scrollToItem(10) + } + // Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls + scrollBehavior.state.heightOffset = 0f + lazyListStateTarget.animateScrollToItem(0) + } + } else { + state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item)) + } + }, + floatingActionButton = { + when (state.currentHomeNavigationBarItem) { HomeNavigationBarItem.Chats -> { - { - HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) - } + HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) } - HomeNavigationBarItem.Spaces -> if (state.homeSpacesState.canCreateSpaces) { - { - HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) - } - } else { - // No FAB for spaces if we cannot create spaces - null + HomeNavigationBarItem.Spaces -> { + HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) } - }, - ) - } else { - HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room) - } + } + }, + ) }, - floatingActionButtonPosition = if (state.showNavigationBar) FabPosition.Center else FabPosition.End, + floatingActionButtonPosition = FabPosition.Center, content = { padding -> val contentPadding = PaddingValues( bottom = 96.dp, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index e2598a9e1c..f541417104 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -60,6 +60,7 @@ import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.unreadIndicator +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.components.InviteSenderView import io.element.android.libraries.matrix.ui.model.InviteSender @@ -349,6 +350,7 @@ private fun MessagePreviewAndIndicatorRow( if (room.hasRoomCall) { OnGoingCallIcon( color = tint, + isAudio = room.activeCallIntent == CallIntent.AUDIO ) } if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) { @@ -398,10 +400,11 @@ private fun InviteNameAndIndicatorRow( @Composable private fun OnGoingCallIcon( color: Color, + isAudio: Boolean ) { Icon( modifier = Modifier.size(16.dp), - imageVector = CompoundIcons.VideoCallSolid(), + imageVector = if (isAudio) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(), contentDescription = stringResource(CommonStrings.a11y_notifications_ongoing_call), tint = color, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt index d723d1a424..26054d7e56 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter +import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.roomlist.LatestEventValue @@ -50,6 +51,11 @@ class RoomListRoomSummaryFactory( avatarData = avatarData, userDefinedNotificationMode = roomInfo.userDefinedNotificationMode, hasRoomCall = roomInfo.hasRoomCall, + activeCallIntent = when (val consensus = roomInfo.activeCallIntentConsensus) { + is CallIntentConsensus.Full -> consensus.callIntent + is CallIntentConsensus.Partial -> consensus.callIntent + CallIntentConsensus.None -> null + }, isDirect = roomInfo.isDirect, isFavorite = roomInfo.isFavorite, inviteSender = roomInfo.inviter?.toInviteSender(), diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt index a59e444455..628d0d0a9b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt @@ -13,6 +13,7 @@ import io.element.android.features.invite.api.InviteData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.model.InviteSender import kotlinx.collections.immutable.ImmutableList @@ -33,6 +34,7 @@ data class RoomListRoomSummary( val avatarData: AvatarData, val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, + val activeCallIntent: CallIntent?, val isDirect: Boolean, val isDm: Boolean, val isFavorite: Boolean, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt index 400decff6f..eefb2d6484 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.ui.model.InviteSender import kotlinx.collections.immutable.toImmutableList @@ -132,6 +133,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider { @Composable override fun present(): SpaceFiltersState { - val isFeatureEnabled by featureFlagService - .isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters) - .collectAsState(initial = false) - val availableFilters by remember { matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } }.collectAsState(initial = persistentListOf()) - if (!isFeatureEnabled || availableFilters.isEmpty()) { + if (availableFilters.isEmpty()) { return SpaceFiltersState.Disabled } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt index 707ac73261..b758a1e593 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -15,8 +15,6 @@ import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import kotlinx.collections.immutable.persistentListOf @@ -29,11 +27,9 @@ import kotlinx.coroutines.flow.map class HomeSpacesPresenter( private val client: MatrixClient, private val seenInvitesStore: SeenInvitesStore, - private val featureFlagsService: FeatureFlagService, ) : Presenter { @Composable override fun present(): HomeSpacesState { - val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val hideInvitesAvatar by client.rememberHideInvitesAvatar() val spaceRooms by remember { client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() } @@ -52,7 +48,6 @@ class HomeSpacesPresenter( spaceRooms = spaceRooms, seenSpaceInvites = seenSpaceInvites, hideInvitesAvatar = hideInvitesAvatar, - canCreateSpaces = canCreateSpaces, // TODO enable once we can link to the screen to explore public spaces canExploreSpaces = false, eventSink = ::handleEvent, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt index 84b2dc7f52..e93f04291e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt @@ -18,7 +18,6 @@ data class HomeSpacesState( val spaceRooms: ImmutableList, val seenSpaceInvites: ImmutableSet, val hideInvitesAvatar: Boolean, - val canCreateSpaces: Boolean, val canExploreSpaces: Boolean, val eventSink: (HomeSpacesEvents) -> Unit, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt index a65f29cc2f..17f2cbad31 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt @@ -30,17 +30,9 @@ open class HomeSpacesStateProvider : PreviewParameterProvider { ), spaceRooms = aListOfSpaceRooms(), ), - aHomeSpacesState( - space = CurrentSpace.Space( - spaceRoom = aSpaceRoom(roomId = RoomId("!mySpace:example.com")) - ), - spaceRooms = aListOfSpaceRooms(), - canCreateSpaces = false, - ), aHomeSpacesState( space = CurrentSpace.Root, spaceRooms = emptyList(), - canCreateSpaces = true, ), ) } @@ -50,7 +42,6 @@ internal fun aHomeSpacesState( spaceRooms: List = aListOfSpaceRooms(), seenSpaceInvites: Set = emptySet(), hideInvitesAvatar: Boolean = false, - canCreateSpaces: Boolean = true, canExploreSpaces: Boolean = true, eventSink: (HomeSpacesEvents) -> Unit = {}, ) = HomeSpacesState( @@ -58,7 +49,6 @@ internal fun aHomeSpacesState( spaceRooms = spaceRooms.toImmutableList(), seenSpaceInvites = seenSpaceInvites.toImmutableSet(), hideInvitesAvatar = hideInvitesAvatar, - canCreateSpaces = canCreateSpaces, canExploreSpaces = canExploreSpaces, eventSink = eventSink, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt index c563e6eb26..9125912bf0 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -55,7 +55,7 @@ fun HomeSpacesView( onExploreClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.canCreateSpaces && state.spaceRooms.isEmpty()) { + if (state.spaceRooms.isEmpty()) { EmptySpaceHomeView( modifier = modifier.padding(contentPadding), onCreateSpaceClick = onCreateSpaceClick, diff --git a/features/home/impl/src/main/res/values-cs/translations.xml b/features/home/impl/src/main/res/values-cs/translations.xml index 16f9e6b801..0276db1fbe 100644 --- a/features/home/impl/src/main/res/values-cs/translations.xml +++ b/features/home/impl/src/main/res/values-cs/translations.xml @@ -5,9 +5,9 @@ "Nepřicházejí vám oznámení?" "Váš zvuk oznámení byl aktualizován – je jasnější, rychlejší a méně rušivý." "Aktualizovali jsme vaše zvuky" - "Vygenerujte nový klíč pro obnovení, který lze použít k obnovení historie šifrovaných zpráv v případě, že ztratíte přístup ke svým zařízením." - "Nastavení obnovy" - "Nastavení obnovy" + "Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení." + "Získat klíč pro obnovení" + "Zálohujte své chaty" "Potvrďte klíč pro obnovení, abyste zachovali přístup k úložišti klíčů a historii zpráv." "Zadejte klíč pro obnovení" "Zapomněli jste klíč pro obnovení?" diff --git a/features/home/impl/src/main/res/values-da/translations.xml b/features/home/impl/src/main/res/values-da/translations.xml index cd9ec06ada..fab528ce71 100644 --- a/features/home/impl/src/main/res/values-da/translations.xml +++ b/features/home/impl/src/main/res/values-da/translations.xml @@ -5,9 +5,9 @@ "Modtager du ikke notifikationer?" "Dit notifikationsping er blevet opdateret – tydeligere, hurtigere og mindre forstyrrende." "Vi har opdateret dine lyde" - "Gendan din kryptografiske identitet og meddelelseshistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder." - "Opsæt gendannelse" - "Konfigurer gendannelse for at beskytte din konto" + "Dine chats sikkerhedskopieres automatisk med end-to-end-kryptering. For at kunne gendanne denne sikkerhedskopi og bevare din digitale identitet, hvis du mister adgang til alle dine enheder, får du brug for din gendannelsesnøgle." + "Hent gendannelsesnøgle" + "Sikkerhedskopier dine samtaler" "Bekræft din gendannelsesnøgle for at bevare adgangen til nøglelager og meddelelseshistorik." "Indtast din gendannelsesnøgle" "Har du glemt din gendannelsesnøgle?" diff --git a/features/home/impl/src/main/res/values-it/translations.xml b/features/home/impl/src/main/res/values-it/translations.xml index 932545c7b0..2c5461bfa8 100644 --- a/features/home/impl/src/main/res/values-it/translations.xml +++ b/features/home/impl/src/main/res/values-it/translations.xml @@ -5,9 +5,9 @@ "Le notifiche non arrivano?" "Il ping delle notifiche è stato aggiornato: ora è più chiaro, più rapido e meno fastidioso." "Abbiamo rinnovato i tuoi suoni" - "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i tuoi dispositivi." - "Configura il recupero" - "Configura il ripristino" + "Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero." + "Ottieni la chiave di recupero" + "Esegui il backup delle tue conversazioni" "Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi." "Inserisci la tua chiave di recupero" "Hai dimenticato la chiave di recupero?" @@ -50,6 +50,7 @@ Non hai messaggi non letti!" "Segna come letto" "Segna come non letto" "Questa stanza è stata aggiornata" + "I tuoi spazi" "Sembra che tu stia usando un nuovo dispositivo. Verificati con un altro dispositivo per accedere ai tuoi messaggi cifrati." "Verifica che sei tu" diff --git a/features/home/impl/src/main/res/values-ja/translations.xml b/features/home/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..3e4b16b682 --- /dev/null +++ b/features/home/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,56 @@ + + + "すべての通知を確実に受信するために、このアプリのバッテリー最適化を無効にしてください。" + "最適化を無効にする" + "通知が届いていませんか?" + "通知音が更新され、より明確で速く、そして邪魔にならなくなりました。" + "サウンドを刷新しました" + "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" + "回復鍵を作成" + "チャットをバックアップ" + "鍵の保管庫と過去のメッセージにアクセスするために、回復鍵を認証してください。" + "回復鍵を入力してください" + "回復鍵を忘れましたか?" + "鍵の保管庫を同期できません。" + "重要な電話を確実に受け取るため、端末がロックされている状態での全画面通知を、設定から許可してください。" + "通話品質を高める" + "チャット" + "スペース" + "%1$sへの招待を本当に破棄しますか?" + "招待を破棄" + "%1$sとのチャットを本当に拒否しますか?" + "チャットを拒否" + "招待はありません" + "%1$s (%2$s) があなたを招待しました" + "一度限りの工程です。お待ちください。" + "アカウントを設定しています。" + "新しい会話またはルームを作成" + "フィルターを解除" + "誰かにメッセージを送信しましょう。" + "まだチャットがありません。" + "お気に入り" + "チャットの設定からお気に入りに追加できます。 +現在は、フィルターの選択を解除することで他のチャットを表示できます。" + "お気に入りのチャットはまだありません" + "招待" + "承認待ちの招待はありません" + "低い優先度" + "低い優先度のチャットはまだありません" + "フィルターを解除して他のチャットを表示できます" + "この選択中にチャットがありません" + "人" + "まだダイレクトメッセージは届いていません" + "ルーム" + "まだルームに参加していません" + "未読" + "やった! +未読メッセージはありません。" + "参加リクエストを送信しました" + "チャット" + "既読にする" + "未読にする" + "このルームはアップグレードされました" + "あなたのスペース" + "新しいデバイスをご利用のようです。暗号化されたメッセージにアクセスするには、別のデバイスで検証してください。" + "本人確認" + diff --git a/features/home/impl/src/main/res/values-ru/translations.xml b/features/home/impl/src/main/res/values-ru/translations.xml index d06c9a3854..233e8749b4 100644 --- a/features/home/impl/src/main/res/values-ru/translations.xml +++ b/features/home/impl/src/main/res/values-ru/translations.xml @@ -5,7 +5,7 @@ "Уведомления не приходят?" "Ваши уведомления были обновлены — теперь они понятнее, быстрее и менее отвлекающие." "Мы обновили ваши звуки" - "Создайте новый ключ восстановления, который можно использовать для восстановления зашифрованной истории сообщений в случае потери доступа к своим устройствам." + "Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления." "Получить ключ восстановления" "Сделайте резервную копию своих чатов." "Подтвердите ключ восстановления, чтобы сохранить доступ к хранилищу ключей и истории сообщений." diff --git a/features/home/impl/src/main/res/values-vi/translations.xml b/features/home/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..bc62880fff --- /dev/null +++ b/features/home/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,48 @@ + + + "Tắt tính năng tối ưu hóa pin cho ứng dụng này để đảm bảo nhận được mọi thông báo." + "Tắt tối ưu hóa" + "Có nhận được thông báo không?" + "Thông báo của bạn đã được cập nhật — rõ ràng hơn, nhanh hơn và ít gây khó chịu hơn." + "Chúng tôi đã làm mới âm thanh của bạn." + "Các cuộc trò chuyện của bạn được tự động sao lưu bằng mã hóa đầu cuối. Để khôi phục bản sao lưu này và giữ lại danh tính kỹ thuật số của bạn khi bạn mất quyền truy cập vào tất cả các thiết bị, bạn sẽ cần khóa khôi phục." + "Lấy khóa khôi phục." + "Sao lưu tin nhắn của bạn" + "Xác nhận khóa khôi phục để không bị mất quyền truy cập vào tin nhắn." + "Nhập khóa khôi phục của bạn." + "Bạn quên khóa khôi phục?”" + "Dữ liệu khóa của bạn không còn đồng bộ" + "Cuộc trò chuyện" + "Bạn có chắc muốn từ chối lời mời tham gia %1$s không?" + "Từ chối lời mời" + "Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?" + "Từ chối trò chuyện" + "Không có lời mời" + "%1$s(%2$s ) đã mời bạn" + "Quá trình này chỉ thực hiện một lần, cảm ơn bạn đã kiên nhẫn." + "Đang thiết lập tài khoản của bạn." + "Tạo một cuộc trò chuyện hoặc phòng mới" + "Bắt đầu bằng cách nhắn tin cho ai đó." + "Chưa có cuộc trò chuyện nào." + "Yêu thích" + "Bạn có thể thêm cuộc trò chuyện vào mục yêu thích trong cài đặt chat. +Hiện tại, bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác." + "Bạn chưa có cuộc trò chuyện yêu thích nào." + "Lời mời" + "Ưu tiên thấp" + "Bạn có thể bỏ chọn bộ lọc để xem các cuộc trò chuyện khác" + "Bạn không có cuộc trò chuyện nào cho lựa chọn này" + "Danh bạ" + "Bạn chưa có tin nhắn riêng nào cả" + "Phòng" + "Bạn chưa tham gia phòng nào" + "Chưa đọc" + "Chúc mừng! +Bạn không còn tin nhắn nào chưa đọc nữa!" + "Yêu cầu tham gia đã được gửi" + "Cuộc trò chuyện" + "Đánh dấu đã đọc" + "Đánh dấu chưa đọc" + "Có vẻ như bạn đang sử dụng thiết bị mới. Hãy xác minh bằng một thiết bị khác để truy cập tin nhắn được mã hóa của bạn." + "Xác thực danh tính của bạn" + diff --git a/features/home/impl/src/main/res/values-zh/translations.xml b/features/home/impl/src/main/res/values-zh/translations.xml index 1705569a49..58e5b1eb91 100644 --- a/features/home/impl/src/main/res/values-zh/translations.xml +++ b/features/home/impl/src/main/res/values-zh/translations.xml @@ -6,7 +6,7 @@ "您的通知提示音已升级 - 更清晰、更快速、干扰更少。" "我们已更新您的声音" "生成新的恢复密钥,该密钥可用于在您无法访问设备时恢复加密的消息历史记录。" - "设置恢复" + "获取恢复密钥" "设置恢复" "确认恢复密钥,以保持对密钥存储和消息历史的访问。" "输入恢复密钥" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 4002844947..371a718523 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -9,14 +9,11 @@ package io.element.android.features.home.impl import com.google.common.truth.Truth.assertThat -import io.element.android.features.announcement.api.Announcement -import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.roomlist.aRoomListState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.home.impl.spaces.aHomeSpacesState import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability -import io.element.android.features.rageshake.test.logs.FakeAnnouncementService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.indicator.api.IndicatorService @@ -33,10 +30,7 @@ import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData -import io.element.android.tests.testutils.MutablePresenter import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -79,7 +73,6 @@ class HomePresenterTest { MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL) ) assertThat(withUserState.showAvatarIndicator).isFalse() - assertThat(withUserState.showNavigationBar).isTrue() } } @@ -139,14 +132,10 @@ class HomePresenterTest { @Test fun `present - NavigationBar change`() = runTest { - val showAnnouncementResult = lambdaRecorder { } val presenter = createHomePresenter( sessionStore = InMemorySessionStore( updateUserProfileResult = { _, _, _ -> }, ), - announcementService = FakeAnnouncementService( - showAnnouncementResult = showAnnouncementResult, - ) ) presenter.test { val initialState = awaitItem() @@ -154,38 +143,6 @@ class HomePresenterTest { initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) val finalState = awaitItem() assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) - showAnnouncementResult.assertions().isCalledOnce() - .with(value(Announcement.Space)) - } - } - - @Test - fun `present - NavigationBar is hidden when the last space is left when the user can't create new spaces`() = runTest { - val homeSpacesPresenter = MutablePresenter(aHomeSpacesState()) - val presenter = createHomePresenter( - sessionStore = InMemorySessionStore( - updateUserProfileResult = { _, _, _ -> }, - ), - homeSpacesPresenter = homeSpacesPresenter, - announcementService = FakeAnnouncementService( - showAnnouncementResult = {}, - ) - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) - assertThat(initialState.showNavigationBar).isTrue() - // User navigate to Spaces - initialState.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces)) - val spaceState = awaitItem() - assertThat(spaceState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Spaces) - // The last space is left - homeSpacesPresenter.updateState(aHomeSpacesState(spaceRooms = emptyList(), canCreateSpaces = false)) - skipItems(1) - val finalState = awaitItem() - // We are back to Chats - assertThat(finalState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats) - assertThat(finalState.showNavigationBar).isFalse() } } } @@ -198,7 +155,6 @@ internal fun createHomePresenter( indicatorService: IndicatorService = FakeIndicatorService(), homeSpacesPresenter: Presenter = Presenter { aHomeSpacesState() }, sessionStore: SessionStore = InMemorySessionStore(), - announcementService: AnnouncementService = FakeAnnouncementService(), ) = HomePresenter( client = client, syncService = syncService, @@ -209,5 +165,4 @@ internal fun createHomePresenter( logoutPresenter = { aDirectLogoutState() }, rageshakeFeatureAvailability = rageshakeFeatureAvailability, sessionStore = sessionStore, - announcementService = announcementService, ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt index 28e7051a55..63f1ecebd7 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt @@ -101,6 +101,7 @@ internal fun createRoomListRoomSummary( displayType = displayType, userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = false, + activeCallIntent = null, isDirect = false, isFavorite = isFavorite, canonicalAlias = null, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt index 278a268864..31a47d830f 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt @@ -8,8 +8,6 @@ package io.element.android.features.home.impl.spacefilters import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.spaces.FakeSpaceService @@ -21,26 +19,9 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class SpaceFiltersPresenterTest { - @Test - fun `present - when feature flag is disabled returns Disabled state`() = runTest { - val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false) - ) - ) - presenter.test { - val state = awaitItem() - assertThat(state).isEqualTo(SpaceFiltersState.Disabled) - } - } - @Test fun `present - when available filters is empty returns Disabled state`() = runTest { - val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ) - ) + val presenter = createSpaceFiltersPresenter() presenter.test { val state = awaitLastSequentialItem() assertThat(state).isEqualTo(SpaceFiltersState.Disabled) @@ -48,15 +29,12 @@ class SpaceFiltersPresenterTest { } @Test - fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest { + fun `present - when filters exist returns Unselected state`() = runTest { val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") val spaceService = FakeSpaceService() val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -75,9 +53,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -99,9 +74,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -129,9 +101,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -159,9 +128,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -196,9 +162,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -224,9 +187,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -271,9 +231,6 @@ class SpaceFiltersPresenterTest { val matrixClient = FakeMatrixClient(spaceService = spaceService) val presenter = createSpaceFiltersPresenter( - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) - ), matrixClient = matrixClient, ) presenter.test { @@ -302,11 +259,9 @@ class SpaceFiltersPresenterTest { } private fun createSpaceFiltersPresenter( - featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), matrixClient: FakeMatrixClient = FakeMatrixClient(), ): SpaceFiltersPresenter { return SpaceFiltersPresenter( - featureFlagService = featureFlagService, matrixClient = matrixClient, ) } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt index 43d3a8896d..c7608833ac 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenterTest.kt @@ -11,9 +11,6 @@ package io.element.android.features.home.impl.spaces import com.google.common.truth.Truth.assertThat import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.test.InMemorySeenInvitesStore -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.test @@ -26,25 +23,18 @@ class HomeSpacesPresenterTest { val presenter = createPresenter() presenter.test { val state = awaitItem() - // canCreateSpaces is initially false - assertThat(state.canCreateSpaces).isFalse() assertThat(state.space).isEqualTo(CurrentSpace.Root) assertThat(state.spaceRooms).isEmpty() assertThat(state.hideInvitesAvatar).isFalse() assertThat(state.seenSpaceInvites).isEmpty() - - // It'll eventually be true - assertThat(awaitItem().canCreateSpaces).isTrue() } } private fun createPresenter( client: MatrixClient = FakeMatrixClient(), seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), - featureFlagsService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.CreateSpaces.key to true)), ) = HomeSpacesPresenter( client = client, seenInvitesStore = seenInvitesStore, - featureFlagsService = featureFlagsService, ) } diff --git a/features/invite/impl/src/main/res/values-be/translations.xml b/features/invite/impl/src/main/res/values-be/translations.xml index fca38b796a..3c17aa0cdb 100644 --- a/features/invite/impl/src/main/res/values-be/translations.xml +++ b/features/invite/impl/src/main/res/values-be/translations.xml @@ -1,10 +1,12 @@ "Заблакіраваць карыстальніка" + "Адхіліць і заблакіраваць" "Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?" "Адхіліць запрашэнне" "Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?" "Адхіліць чат" "Няма запрашэнняў" "%1$s (%2$s) запрасіў(-ла) вас" + "Адхіліць і заблакіраваць" diff --git a/features/invite/impl/src/main/res/values-ja/translations.xml b/features/invite/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5e1b527dd3 --- /dev/null +++ b/features/invite/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,18 @@ + + + "このユーザーからのメッセージと招待を非表示します" + "ユーザーをブロック" + "アカウント提供元にこのルームを報告" + "報告の理由を説明してください…" + "拒否してブロック" + "%1$sへの招待を本当に破棄しますか?" + "招待を破棄" + "%1$sとのチャットを本当に拒否しますか?" + "チャットを拒否" + "招待はありません" + "%1$s (%2$s) があなたを招待しました" + "拒否してブロックする" + "本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。" + "招待を拒否してブロック" + "拒否してブロック" + diff --git a/features/invite/impl/src/main/res/values-vi/translations.xml b/features/invite/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..9b7784c25d --- /dev/null +++ b/features/invite/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,17 @@ + + + "Bạn sẽ không nhận được bất kỳ tin nhắn hoặc lời mời tham gia phòng nào từ người dùng này." + "Chặn người dùng" + "Báo cáo phòng này cho nhà cung cấp tài khoản của bạn." + "Từ chối và chặn" + "Bạn có chắc muốn từ chối lời mời tham gia %1$s không?" + "Từ chối lời mời" + "Bạn có chắc muốn từ chối cuộc trò chuyện riêng với %1$s không?" + "Từ chối trò chuyện" + "Không có lời mời" + "%1$s(%2$s ) đã mời bạn" + "Có, từ chối & chặn" + "Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng." + "Từ chối lời mời và chặn" + "Từ chối và chặn" + diff --git a/features/invite/impl/src/main/res/values-zh/translations.xml b/features/invite/impl/src/main/res/values-zh/translations.xml index e7cf39a9c3..8c27397225 100644 --- a/features/invite/impl/src/main/res/values-zh/translations.xml +++ b/features/invite/impl/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ - "您不会看到来自该用户的任何信息或房间邀请" - "封禁用户" + "您将不会看到来自该用户的任何信息或房间邀请" + "屏蔽用户" "向您的帐户提供商举报此房间。" "描述举报的原因…" "拒绝并屏蔽" @@ -12,7 +12,7 @@ "没有邀请" "%1$s (%2$s)邀请了你" "是的,拒绝并屏蔽" - "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。" + "您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。" "拒绝邀请并屏蔽" "拒绝并屏蔽" diff --git a/features/invitepeople/impl/build.gradle.kts b/features/invitepeople/impl/build.gradle.kts index e2025405ff..390ccce7b9 100644 --- a/features/invitepeople/impl/build.gradle.kts +++ b/features/invitepeople/impl/build.gradle.kts @@ -34,13 +34,15 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) - implementation(projects.libraries.usersearch.impl) + implementation(projects.libraries.usersearch.api) implementation(libs.coil.compose) implementation(projects.services.apperror.api) + implementation(projects.libraries.featureflag.api) api(projects.features.invitepeople.api) testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.services.apperror.test) + testImplementation(projects.libraries.featureflag.test) } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/ConfirmingUnknownUserInvitation.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/ConfirmingUnknownUserInvitation.kt new file mode 100644 index 0000000000..8f4d5e1510 --- /dev/null +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/ConfirmingUnknownUserInvitation.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invitepeople.impl + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +data class ConfirmingUnknownUserInvitation( + val users: ImmutableList +) : AsyncAction.Confirming diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt index b1f18b1df9..449d0ce6ac 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt @@ -14,4 +14,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents { data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents + data object DismissUnknownUsersModal : DefaultInvitePeopleEvents + data object RemoveUnknownUsers : DefaultInvitePeopleEvents } diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt index 3450587e82..58b3fb67f6 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,8 +37,11 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -50,6 +54,7 @@ import io.element.android.services.apperror.api.AppErrorStateService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.launchIn @@ -69,6 +74,7 @@ class DefaultInvitePeoplePresenter( private val coroutineDispatchers: CoroutineDispatchers, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val appErrorStateService: AppErrorStateService, + private val featureFlagService: FeatureFlagService, private val matrixClient: MatrixClient, ) : InvitePeoplePresenter { @AssistedFactory @@ -87,6 +93,8 @@ class DefaultInvitePeoplePresenter( val showSearchLoader = rememberSaveable { mutableStateOf(false) } val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false) + val recentDirectRooms by produceState(emptyList(), roomMembers.value) { if (roomMembers.value.isSuccess()) { val activeMemberIds = roomMembers.value.dataOrNull().orEmpty() @@ -126,6 +134,40 @@ class DefaultInvitePeoplePresenter( } } + val selectedUserIdentities = produceState( + emptyMap().toImmutableMap(), + selectedUsers.value, + enableKeyShareOnInvite, + ) { + if (!enableKeyShareOnInvite) { + return@produceState + } + + val selected = selectedUsers.value + + val cached = value + .filterKeys { it in selected } + + val uncached = selected + .filterNot(cached::containsKey) + .associateWith { user -> + matrixClient.encryptionService + .getUserIdentity(user.userId, fallbackToServer = false) + .getOrNull() + } + + value = (cached + uncached).toImmutableMap() + } + + val unknownUsers by remember { + derivedStateOf { + selectedUserIdentities.value + .filterValues { it == null } + .keys + .toImmutableList() + } + } + LaunchedEffect(room.isSuccess()) { room.dataOrNull()?.let { fetchMembers(it, roomMembers) @@ -144,21 +186,41 @@ class DefaultInvitePeoplePresenter( fun handleEvent(event: InvitePeopleEvents) { when (event) { - is DefaultInvitePeopleEvents.OnSearchActiveChanged -> { - searchActive = event.active - if (!event.active) { - queryState.clearText() + // Dedicated `when` for exhaustivity. + is DefaultInvitePeopleEvents -> when (event) { + is DefaultInvitePeopleEvents.OnSearchActiveChanged -> { + searchActive = event.active + if (!event.active) { + queryState.clearText() + } + } + + is DefaultInvitePeopleEvents.ToggleUser -> { + selectedUsers.toggleUser(event.user) + searchResults.toggleUser(event.user) + // suggestions will automatically update via derivedStateOf when selectedUsers changes + } + is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> { + sendInvitesAction.value = AsyncAction.Uninitialized + } + is DefaultInvitePeopleEvents.RemoveUnknownUsers -> { + val usersToRemove = selectedUsers.value.filter { it in unknownUsers } + usersToRemove.forEach { user -> + selectedUsers.toggleUser(user) + searchResults.toggleUser(user) + } + sendInvitesAction.value = AsyncAction.Uninitialized } } - - is DefaultInvitePeopleEvents.ToggleUser -> { - selectedUsers.toggleUser(event.user) - searchResults.toggleUser(event.user) - // suggestions will automatically update via derivedStateOf when selectedUsers changes - } is InvitePeopleEvents.SendInvites -> { - room.dataOrNull()?.let { - sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) + if (enableKeyShareOnInvite && unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) { + sendInvitesAction.value = ConfirmingUnknownUserInvitation( + unknownUsers + ) + } else { + room.dataOrNull()?.let { + sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) + } } } is InvitePeopleEvents.CloseSearch -> { diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt index 15ded2ae3f..c26b8de254 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt @@ -76,6 +76,16 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider, + onDismiss: () -> Unit, + onInvite: () -> Unit, + onRemove: () -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + dragHandle = null, + ) { + IconTitleSubtitleMolecule( + title = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_title, users.size), + subTitle = pluralStringResource(R.plurals.screen_invite_users_confirm_dialog_subtitle, users.size), + iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()), + modifier = Modifier.padding( + top = 32.dp, + bottom = 16.dp, + start = 16.dp, + end = 16.dp, + ) + ) + + LazyColumn { + items(users) { user -> + MatrixUserRow(user) + } + } + + ButtonRowMolecule( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp) + ) { + OutlinedButton( + text = stringResource(CommonStrings.action_remove), + onClick = onRemove, + leadingIcon = IconSource.Vector(CompoundIcons.Close()), + modifier = Modifier.weight(1f) + ) + Button( + text = stringResource(CommonStrings.action_invite), + onClick = onInvite, + leadingIcon = IconSource.Vector(CompoundIcons.Check()), + modifier = Modifier.weight(1f) + ) + } + } +} + @PreviewsDayNight @Composable internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) = diff --git a/features/invitepeople/impl/src/main/res/values-ja/translations.xml b/features/invitepeople/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..6aa1fb0370 --- /dev/null +++ b/features/invitepeople/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "既に参加しています" + "既に招待しています" + + "この連絡先とのチャットがありません。続行する前に、このルームに招待してください。" + + + "このルームに新しい連絡先を追加しますか?" + + diff --git a/features/invitepeople/impl/src/main/res/values-vi/translations.xml b/features/invitepeople/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..934cb7d24f --- /dev/null +++ b/features/invitepeople/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Đã là thành viên" + "Đã được mời" + diff --git a/features/invitepeople/impl/src/main/res/values/localazy.xml b/features/invitepeople/impl/src/main/res/values/localazy.xml index d89ae92e75..0515121428 100644 --- a/features/invitepeople/impl/src/main/res/values/localazy.xml +++ b/features/invitepeople/impl/src/main/res/values/localazy.xml @@ -2,4 +2,12 @@ "Already a member" "Already invited" + + "You currently don’t have any chats with this contact. Confirm inviting them to this room before continuing." + "You currently don’t have any chats with these contacts. Confirm inviting them to this room before continuing." + + + "Invite a new contact to this room?" + "Invite new contacts to this room?" + diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt index ab9e20437e..a1d72010f6 100644 --- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt +++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt @@ -15,9 +15,13 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState @@ -28,6 +32,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -43,6 +48,7 @@ import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.services.apperror.api.AppErrorStateService import io.element.android.services.apperror.test.FakeAppErrorStateService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -56,6 +62,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +@Suppress("LargeClass") internal class DefaultInvitePeoplePresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -605,6 +612,231 @@ internal class DefaultInvitePeoplePresenterTest { } } + @Test + fun `present - users are prompted for confirmation if they attempt to invite unknown users`() = runTest { + val alice = aMatrixUser("@alice:example.com") + val bob = aMatrixUser("@bob:example.com") + val charlie = aMatrixUser("@charlie:example.com") + + val getUserIdentityResult = lambdaRecorder> { userId -> + when (userId.value) { + alice.userId.value -> Result.success(IdentityState.Pinned) + bob.userId.value -> Result.success(null) + else -> Result.failure(AN_EXCEPTION) + } + } + + val inviteUserResult = lambdaRecorder> { userId: UserId -> + Result.success(Unit) + } + val encryptionService = FakeEncryptionService( + getUserIdentityResult = getUserIdentityResult + ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + inviteUserResult = inviteUserResult, + matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService + ) + presenter.test { + val initialState = awaitItem() + skipItems(1) + + // When we toggle a user not in the list, they are added, and we fetch their identity. + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie)) + delay(100) + + // If we do not have their identity cached, or fail to fetch it, we should mark them as unknown. + awaitItemAsDefault().run { + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + eventSink(InvitePeopleEvents.SendInvites) + } + + getUserIdentityResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + // When we then try to invite these users, we should prompt for confirmation first. + awaitItemAsDefault().run { + assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java) + assertThat(canInvite).isTrue() + eventSink(InvitePeopleEvents.SendInvites) + } + + delay(1_000) + inviteUserResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - selecting remove on confirmation prompt unselects unknown users`() = runTest { + val alice = aMatrixUser("@alice:example.com") + val bob = aMatrixUser("@bob:example.com") + val charlie = aMatrixUser("@charlie:example.com") + + val repository = FakeUserRepository() + + val getUserIdentityResult = lambdaRecorder> { userId -> + when (userId.value) { + alice.userId.value -> Result.success(IdentityState.Pinned) + bob.userId.value -> Result.success(null) + else -> Result.failure(AN_EXCEPTION) + } + } + + val encryptionService = FakeEncryptionService( + getUserIdentityResult = getUserIdentityResult + ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val presenter = createDefaultInvitePeoplePresenter( + userRepository = repository, + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService + ) + presenter.test { + val initialState = awaitItemAsDefault() + skipItems(1) + + // When we toggle a user not in the list, they are added, and we fetch their identity. + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie)) + delay(100) + + // And the search is matching Alice and Bob + initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") + assertThat(repository.providedQuery).isEqualTo("some query") + repository.emitState( + UserSearchResultState( + results = listOf(UserSearchResult(alice), UserSearchResult(bob)), + isSearching = true + ) + ) + skipItems(3) + + awaitItemAsDefault().run { + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + + // Both Alice and Bob are selected in searchResults + assertThat( + searchResults.users().map { Pair(it.matrixUser, it.isSelected) } + ).containsExactly(Pair(alice, true), Pair(bob, true)) + + eventSink(InvitePeopleEvents.SendInvites) + } + + getUserIdentityResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + // When we then try to invite these user, we should prompt for confirmation first. + awaitItemAsDefault().run { + assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java) + assertThat(canInvite).isTrue() + eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers) + } + + // Selecting "remove" should remove all unknown users, but keeps those who are known. + (awaitLastSequentialItem() as DefaultInvitePeopleState).run { + assertThat(sendInvitesAction.isUninitialized()).isTrue() + assertThat(selectedUsers).containsExactly(alice) + + // Bob is no longer selected in searchResults + assertThat( + searchResults.users().map { Pair(it.matrixUser, it.isSelected) } + ).containsExactly(Pair(alice, true), Pair(bob, false)) + } + } + } + + @Test + fun `present - dismissing confirmation prompt does not affect selection`() = runTest { + val alice = aMatrixUser("@alice:example.com") + val bob = aMatrixUser("@bob:example.com") + val charlie = aMatrixUser("@charlie:example.com") + + val getUserIdentityResult = lambdaRecorder> { userId -> + when (userId.value) { + alice.userId.value -> Result.success(IdentityState.Pinned) + bob.userId.value -> Result.success(null) + else -> Result.failure(AN_EXCEPTION) + } + } + + val encryptionService = FakeEncryptionService( + getUserIdentityResult = getUserIdentityResult + ) + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClient = FakeMatrixClient(encryptionService = encryptionService), + featureFlagService = featureFlagService + ) + presenter.test { + val initialState = awaitItem() + skipItems(1) + + // When we toggle a user not in the list, they are added, and we fetch their identity. + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob)) + delay(100) + awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie)) + delay(100) + + awaitItemAsDefault().run { + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + eventSink(InvitePeopleEvents.SendInvites) + } + + getUserIdentityResult.assertions().isCalledExactly(3).withSequence( + listOf(value(alice.userId)), + listOf(value(bob.userId)), + listOf(value(charlie.userId)) + ) + + // When we then try to invite these user, we should prompt for confirmation first. + awaitItemAsDefault().run { + assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java) + assertThat(canInvite).isTrue() + eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal) + } + + // Dismissing should not modify the selection at all + (awaitLastSequentialItem() as DefaultInvitePeopleState).run { + assertThat(sendInvitesAction.isUninitialized()).isTrue() + assertThat(selectedUsers).containsExactly(alice, bob, charlie) + } + } + } + private suspend fun FakeUserRepository.emitStateWithUsers( users: List, isSearching: Boolean = false @@ -646,6 +878,7 @@ fun TestScope.createDefaultInvitePeoplePresenter( userRepository: UserRepository = FakeUserRepository(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), appErrorStateService: AppErrorStateService = FakeAppErrorStateService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), matrixClient: MatrixClient = FakeMatrixClient(), ): DefaultInvitePeoplePresenter { return DefaultInvitePeoplePresenter( @@ -655,6 +888,7 @@ fun TestScope.createDefaultInvitePeoplePresenter( coroutineDispatchers = coroutineDispatchers, sessionCoroutineScope = backgroundScope, appErrorStateService = appErrorStateService, + featureFlagService = featureFlagService, matrixClient = matrixClient, ) } diff --git a/features/joinroom/impl/src/main/res/values-be/translations.xml b/features/joinroom/impl/src/main/res/values-be/translations.xml index 1986a761b0..f166186a1b 100644 --- a/features/joinroom/impl/src/main/res/values-be/translations.xml +++ b/features/joinroom/impl/src/main/res/values-be/translations.xml @@ -1,5 +1,6 @@ + "Адхіліць і заблакіраваць" "Далучыцца" "Націсніце, каб далучыцца" "%1$s пакуль не падтрымлівае прасторы. Вы можаце атрымаць доступ да прастор праз вэб-старонку." diff --git a/features/joinroom/impl/src/main/res/values-ja/translations.xml b/features/joinroom/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f5bb2e3b66 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,34 @@ + + + "%1$s があなたを追放しました。" + "追放されました" + "理由: %1$s" + "リクエストをキャンセル" + "キャンセルします" + "このルームへの参加のリクエストを本当にキャンセルしますか?" + "参加のリクエストをキャンセル" + "拒否してブロックする" + "本当にこのルームへの参加の招待を拒否しますか?%1$s は、あなたと会話することやルームに招待することができなくなります。" + "招待を拒否してブロック" + "拒否してブロック" + "参加に失敗" + "制限付きアクセスまたは招待制です。" + "忘れる" + "参加するには招待が必要です" + "以下のユーザーからの招待" + "参加" + "参加するには、招待またはスペースのメンバーである必要があります。" + "参加をリクエスト" + "文字数制限 %1$d/%2$d 字" + "メッセージ (任意)" + "リクエストが承認された場合はルームへの招待が届きます。" + "参加リクエストを送信しました" + "ルームのプレビューを表示できません。サーバーまたはネットワークの問題の可能性があります。" + "ルームのプレビューを表示できません" + "%1$s はスペースに対応していません。Webからアクセスすることができます。" + "まだスペースに対応していません" + "下のボタンを押すとルーム管理者に通知が届きます。承認の後、会話に参加することができます。" + "過去のメッセージを表示するには、このルームのメンバーである必要があります。" + "ルームに参加しますか?" + "プレビューは利用できません" + diff --git a/features/joinroom/impl/src/main/res/values-vi/translations.xml b/features/joinroom/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..cb9258b308 --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,19 @@ + + + "Hủy yêu cầu" + "Có, hủy" + "Bạn có chắc chắn muốn hủy yêu cầu tham gia phòng này không?" + "Hủy yêu cầu tham gia" + "Có, từ chối & chặn" + "Bạn có chắc muốn từ chối lời mời tham gia phòng này không? Điều này cũng sẽ ngăn %1$s liên hệ với bạn hoặc mời bạn vào các phòng." + "Từ chối lời mời và chặn" + "Từ chối và chặn" + "Được mời bởi" + "Tham gia" + "Số ký tự cho phép: %1$d / %2$d" + "Lời nhắn (tùy chọn)" + "Bạn sẽ nhận được lời mời tham gia phòng nếu yêu cầu của bạn được chấp nhận." + "Yêu cầu tham gia đã được gửi" + "Không thể hiển thị bản xem trước của phòng. Có thể do lỗi mạng hoặc máy chủ." + "Không thể hiển thị bản xem trước của phòng này" + diff --git a/features/joinroom/impl/src/main/res/values-zh/translations.xml b/features/joinroom/impl/src/main/res/values-zh/translations.xml index 7bea362c81..d38f9d3427 100644 --- a/features/joinroom/impl/src/main/res/values-zh/translations.xml +++ b/features/joinroom/impl/src/main/res/values-zh/translations.xml @@ -1,14 +1,14 @@ - "您已被禁止访问%1$s。" - "你已被禁止访问" + "您已被 %1$s 封禁。" + "你已被此房间封禁" "理由:%1$s。" "取消请求" "是的,取消" "您确定要取消加入此房间的请求吗?" "取消加入申请" "是的,拒绝并屏蔽" - "您确定要拒绝加入此房间的邀请吗?这也将阻止%1$s 与您联系或邀请您加入房间。" + "您确定要拒绝加入此房间的邀请吗?这也将阻止 %1$s 与您联系或邀请您加入房间。" "拒绝邀请并屏蔽" "拒绝并屏蔽" "加入失败" @@ -29,6 +29,6 @@ "空间尚不支持" "点击下面的按钮,系统将通知聊天室管理员。获得批准后将能够加入对话。" "只有聊天室成员才能查看消息历史记录。" - "想加入这个聊天室吗?" + "想加入此聊天室吗?" "预览不可用" diff --git a/features/knockrequests/impl/src/main/res/values-ja/translations.xml b/features/knockrequests/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..d08d4f3c20 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,35 @@ + + + "すべて承認" + "本当にすべての参加リクエストを承認しますか?" + "すべてのリクエストを承認" + "すべて承認" + "リクエストの一部を承認できませんでした。もう一度試しますか?" + "リクエストの承認に一部失敗" + "すべてのリクエストを承認中" + "リクエストを承認できませんでした。もう一度試しますか?" + "リクエストの承認に失敗" + "リクエストを承認中" + "拒否して追放する" + "本当に %1$s を拒否して追放しますか?このユーザーが再度リクエストを送信することはできなくなります。" + "拒否してアクセスから追放" + "拒否してアクセスから追放中" + "拒否する" + "本当に %1$s の参加リクエストを拒否しますか?" + "アクセスを拒否" + "拒否と追放" + "このリクエストを拒否できません。もう一度試しますか?" + "リクエストの拒否に失敗" + "参加リクエストを拒否中" + "ルームへの参加リクエストがある場合は、ここに表示されます。" + "参加リクエストがありません" + "参加リクエストを読み込み中" + "参加のリクエスト" + + "%1$s 他 %2$d 人がルーム参加を希望" + + "すべて表示" + "承諾" + "%1$s がこのルームの参加を要求しています" + "表示" + diff --git a/features/knockrequests/impl/src/main/res/values-vi/translations.xml b/features/knockrequests/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..a80aa6fbb8 --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,29 @@ + + + "Có, chấp nhận tất cả" + "Bạn có chắc chắn muốn chấp nhận tất cả các yêu cầu tham gia không?" + "Chấp nhận tất cả các yêu cầu" + "Chấp nhận tất cả" + "Không thể chấp nhận tất cả yêu cầu. Bạn có muốn thử lại không?" + "Chấp nhận tất cả yêu cầu thất bại" + "Đang duyệt tất cả yêu cầu tham gia" + "Không thể chấp nhận yêu cầu này. Bạn có muốn thử lại không?" + "Chấp nhận yêu cầu thất bại" + "Đang duyệt yêu cầu tham gia" + "Có, từ chối và cấm" + "Bạn có chắc muốn từ chối và cấm %1$s không? Người dùng này sẽ không thể yêu cầu tham gia phòng này nữa" + "Từ chối và cấm truy cập" + "Đang từ chối và chặn truy cập" + "Có, từ chối" + "Bạn có chắc muốn từ chối yêu cầu tham gia phòng của %1$s không?" + "Từ chối truy cập" + "Từ chối và chặn" + "Không thể từ chối yêu cầu. Bạn có muốn thử lại không?" + "Từ chối yêu cầu thất bại" + "Đang từ chối yêu cầu tham gia" + "Khi ai đó xin vào phòng, bạn sẽ thấy yêu cầu ở đây." + "Không có yêu cầu tham gia nào đang chờ xử lý" + "Đang tải các yêu cầu tham gia…" + "Đồng ý" + "Xem" + diff --git a/features/knockrequests/impl/src/main/res/values-zh/translations.xml b/features/knockrequests/impl/src/main/res/values-zh/translations.xml index f568ee1120..08718dfc5a 100644 --- a/features/knockrequests/impl/src/main/res/values-zh/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-zh/translations.xml @@ -30,6 +30,6 @@ "查看全部" "接受" - "%1$s想加入这个房间" + "%1$s 想加入此房间" "查看" diff --git a/features/leaveroom/api/src/main/res/values-ja/translations.xml b/features/leaveroom/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..73c1508bd2 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,10 @@ + + + "本当にこの会話を退出しますか?この会話は非公開で、再度参加するには招待が必要です。" + "本当にこのルームを退出しますか?あなたが最後の一人であり、このルームには誰も参加することができなくなります。" + "ルームから退出してもよいですか? このルームは非公開のため、参加しなおすには改めて招待される必要があります。" + "所有者を選択" + "あなたがこのルームの唯一の所有者です。退出する前に所有権を他のユーザーへ譲与する必要があります。" + "所有権の譲与" + "本当にこのルームを退出しますか?" + diff --git a/features/leaveroom/api/src/main/res/values-vi/translations.xml b/features/leaveroom/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..d25a7e34b2 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Bạn có chắc chắn muốn rời khỏi cuộc trò chuyện này không? Cuộc trò chuyện này không công khai và bạn sẽ không thể tham gia lại nếu không được mời." + "Bạn có chắc chắn muốn rời khỏi phòng này không? Bạn là người duy nhất ở đây. Nếu bạn rời đi, sẽ không ai có thể tham gia nữa, kể cả bạn." + "Bạn có chắc chắn muốn rời khỏi phòng này không? Phòng này không công khai và bạn sẽ không thể tham gia lại nếu không có lời mời." + "Bạn có chắc chắn muốn rời khỏi phòng không?" + diff --git a/features/linknewdevice/impl/src/main/res/values-it/translations.xml b/features/linknewdevice/impl/src/main/res/values-it/translations.xml index 0cf548c19b..9a6476823a 100644 --- a/features/linknewdevice/impl/src/main/res/values-it/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-it/translations.xml @@ -1,16 +1,33 @@ "Scansiona il codice QR" + "Apri %1$s su un laptop o un computer desktop" "Scansiona il codice QR con questo dispositivo" "Pronto per la scansione" + "Apri %1$s su un computer desktop per ottenere il codice QR" + "I numeri non corrispondono" + "Inserisci il codice a 2 cifre" + "Questo verificherà che la connessione con l\'altro dispositivo sia sicura." + "Inserisci il numero visualizzato sull\'altro dispositivo" "Il tuo fornitore di account non supporta %1$s." "%1$s non supportato" + "Il tuo provider di account non supporta l\'accesso a un nuovo dispositivo tramite codice QR." "Codice QR non supportato" "L\'accesso è stato annullato sull\'altro dispositivo." "Richiesta di accesso annullata" "L\'accesso è scaduto. Riprova." "L\'accesso non è stato completato in tempo" + "Apri %1$s sull\'altro dispositivo" "Seleziona %1$s" + "“Accedi con codice QR”" + "Scansiona il codice QR qui riportato con l\'altro dispositivo" + "Apri %1$s sull\'altro dispositivo" + "Computer desktop" + "Caricamento codice QR in corso…" + "Dispositivo mobile" + "Che tipo di dispositivo desideri collegare?" + "Prova di nuovo e assicurati di aver inserito correttamente il codice a 2 cifre. Se i numeri continuano a non corrispondere, contatta il gestore del tuo account." + "I numeri non corrispondono" "Non è stato possibile stabilire una connessione sicura con il nuovo dispositivo. I tuoi dispositivi esistenti sono ancora al sicuro e non devi preoccuparti di loro." "E adesso?" "Prova ad accedere di nuovo con un codice QR nel caso si sia verificato un problema di rete." @@ -21,6 +38,8 @@ "Richiesta di accesso annullata" "L\'accesso è stato rifiutato sull\'altro dispositivo." "Accesso rifiutato" + "Non devi fare altro." + "L\'altro tuo dispositivo è già connesso" "L\'accesso è scaduto. Riprova." "L\'accesso non è stato completato in tempo" "L\'altro dispositivo non supporta l\'accesso a %s con un codice QR. diff --git a/features/linknewdevice/impl/src/main/res/values-ja/translations.xml b/features/linknewdevice/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..6cfd5baf84 --- /dev/null +++ b/features/linknewdevice/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,57 @@ + + + "QRコードを読み取り" + "%1$s をコンピュータで開いてください" + "この端末でQRコードを読み取る" + "読み取る" + "%1$s をコンピュータで開き、QRコードを表示してください" + "数字が一致しません" + "2桁の数字を入力してください" + "他の端末との接続が安全であることを確認します。" + "一方の端末で表示される数字を入力してください" + "アカウント提供元が %1$s に対応していません。" + "%1$s に非対応" + "あなたのアカウント提供元は、QRコードによる追加のサインインに対応していません。" + "QRコードに非対応" + "もう一方の端末がサインインをキャンセルしました" + "サインインのリクエストがキャンセルされました" + "サインインが無効です。もう一度試してください。" + "サインインが時間内に完了しませんでした" + "%1$s を他の端末で開いてください" + "%1$s を選択してください" + "\"QRコードでサインイン\"" + "表示されているQRコードを一方の端末で読み取ってください" + "%1$s を他の端末で開いてください" + "コンピュータ" + "QRコードを読み込み中…" + "モバイル端末" + "どのような端末を使用してサインインしますか?" + "入力した2桁の数字が正しいことを確認し、再度試してください。問題が継続する場合はアカウント提供元に問い合わせてください。" + "数字が一致しません" + "新しい端末で安全な通信を確立できませんでした。既存の端末は安全な状態を維持しています。" + "どうしますか?" + "ネットワークの問題の可能性があるため、再度QRコードでログインを試してください。" + "同様の問題が発生する場合は、異なるWi-Fiやモバイルデータ通信を試してください" + "問題が解決しない場合は、手動でサインインしてください" + "接続が安全ではありません" + "もう一方の端末がサインインをキャンセルしました" + "サインインのリクエストがキャンセルされました" + "もう一方の端末でサインインを拒否されました" + "サインインを拒否" + "他には何もする必要はありません。" + "他の端末で既にサインインしています" + "サインインが無効です。もう一度試してください。" + "サインインが時間内に完了しませんでした" + "QRコードを使用した %s へのサインインに他の端末が対応していません。 + +異なる端末でQRコードを読み取るか、手動でサインインしてください。" + "QRコードに非対応" + "アカウント提供元が %1$s に対応していません。" + "%1$s に非対応" + "もう一方の端末に表示されているQRコードを使用してください" + "もう一度やり直してください" + "QRコードが間違っています" + "続行するには、%1$s にカメラの使用を許可する必要があります。" + "QRコードを読み取るため、カメラへのアクセスを許可" + "予期せぬ問題が発生しました。もう一度試してください。" + diff --git a/features/linknewdevice/impl/src/main/res/values-vi/translations.xml b/features/linknewdevice/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5e7709f814 --- /dev/null +++ b/features/linknewdevice/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "Thử lại" + diff --git a/features/linknewdevice/impl/src/main/res/values-zh/translations.xml b/features/linknewdevice/impl/src/main/res/values-zh/translations.xml index 843395fab7..10ae1ae7a7 100644 --- a/features/linknewdevice/impl/src/main/res/values-zh/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-zh/translations.xml @@ -23,7 +23,7 @@ "请用另一台设备扫描此处显示的二维码" "在另一台设备上打开 %1$s" "台式计算机" - "正在加载 QR 码…" + "正在加载二维码…" "移动设备" "您想连接哪种类型的设备?" "请重试,并确保您已正确输入两位验证码。如果验证码仍然不匹配,请联系您的账户提供商。" diff --git a/features/location/impl/src/main/res/values-cs/translations.xml b/features/location/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..99deeba029 --- /dev/null +++ b/features/location/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,4 @@ + + + "Zvolte, jak dlouho chcete sdílet svou aktuální polohu." + diff --git a/features/location/impl/src/main/res/values-da/translations.xml b/features/location/impl/src/main/res/values-da/translations.xml new file mode 100644 index 0000000000..f15ab0fb2f --- /dev/null +++ b/features/location/impl/src/main/res/values-da/translations.xml @@ -0,0 +1,4 @@ + + + "Vælg, hvor længe du vil dele din aktuelle position." + diff --git a/features/location/impl/src/main/res/values-fr/translations.xml b/features/location/impl/src/main/res/values-fr/translations.xml index 46689488e1..043c180dbc 100644 --- a/features/location/impl/src/main/res/values-fr/translations.xml +++ b/features/location/impl/src/main/res/values-fr/translations.xml @@ -1,4 +1,5 @@ + "Votre historique de localisation en direct sera enregistré dans le salon et visible par les membres après la fin de la session." "Choisissez la durée pendant laquelle vous partagerez votre position en direct." diff --git a/features/location/impl/src/main/res/values-hu/translations.xml b/features/location/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..b89965485f --- /dev/null +++ b/features/location/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,5 @@ + + + "Az élő helymeghatározás története a szobában lesz tárolva, és a munkamenet befejezése után is látható marad a tagok számára." + "Válassza ki, mennyi ideig szeretné megosztani az aktuális tartózkodási helyét." + diff --git a/features/location/impl/src/main/res/values-it/translations.xml b/features/location/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..235a9eba4a --- /dev/null +++ b/features/location/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,5 @@ + + + "La cronologia delle tue posizioni in tempo reale verrà archiviata nella stanza e sarà visibile ai membri al termine della sessione." + "Scegli per quanto tempo condividere la tua posizione in tempo reale." + diff --git a/features/location/impl/src/main/res/values-ja/translations.xml b/features/location/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..971693ef11 --- /dev/null +++ b/features/location/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "ライブ位置情報の履歴はルームに保管され、メンバーは後から確認することもできます。" + "ライブ位置情報を共有する期間を選択してください。" + diff --git a/features/location/impl/src/main/res/values-ko/translations.xml b/features/location/impl/src/main/res/values-ko/translations.xml new file mode 100644 index 0000000000..e283d20b9c --- /dev/null +++ b/features/location/impl/src/main/res/values-ko/translations.xml @@ -0,0 +1,5 @@ + + + "실시간 위치 기록은 대화방에 저장되며, 공유 종료 후에도 멤버들이 확인할 수 있습니다." + "실시간 위치를 공유할 시간을 선택해 주세요." + diff --git a/features/location/impl/src/main/res/values-ru/translations.xml b/features/location/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..3c496f1d39 --- /dev/null +++ b/features/location/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,5 @@ + + + "История вашего местоположения в режиме реального времени будет сохранена в комнате и станет доступна участникам после окончания сессии." + "Выберите, как долго вы будете делиться своим местоположением в режиме реального времени." + diff --git a/features/location/impl/src/main/res/values/localazy.xml b/features/location/impl/src/main/res/values/localazy.xml index 04538049db..ac2ff4b2a0 100644 --- a/features/location/impl/src/main/res/values/localazy.xml +++ b/features/location/impl/src/main/res/values/localazy.xml @@ -1,4 +1,5 @@ + "Your live location history will be stored in the room and visible to members after the session ends." "Choose how long to share your live location." diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt index 92c27d9f21..46bc4a55df 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -6,6 +6,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.location.impl.share import app.cash.molecule.RecompositionMode @@ -37,6 +39,7 @@ import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule diff --git a/features/lockscreen/impl/src/main/res/values-cs/translations.xml b/features/lockscreen/impl/src/main/res/values-cs/translations.xml index fce1142f2c..f54d16f42b 100644 --- a/features/lockscreen/impl/src/main/res/values-cs/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-cs/translations.xml @@ -23,7 +23,7 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a "Zadejte stejný PIN dvakrát" "PIN kódy se neshodují." "Abyste mohli pokračovat, budete se muset znovu přihlásit a vytvořit nový PIN" - "Jste odhlášeni" + "Toto zařízení se odstraňuje" "Máte %1$d pokus pro odemknutí" "Máte %1$d pokusy pro odemknutí" @@ -36,5 +36,5 @@ Vyberte si něco zapamatovatelného. Pokud tento kód PIN zapomenete, budete z a "Použijte biometrické údaje" "Použít PIN" - "Odhlašování…" + "Odebírání zařízení…" diff --git a/features/lockscreen/impl/src/main/res/values-da/translations.xml b/features/lockscreen/impl/src/main/res/values-da/translations.xml index ffbd657970..76725ac637 100644 --- a/features/lockscreen/impl/src/main/res/values-da/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-da/translations.xml @@ -23,7 +23,7 @@ Vælg noget mindeværdigt. Hvis du glemmer denne pinkode, bliver du logget ud af "Indtast venligst den samme PIN-kode to gange" "PIN-koderne stemmer ikke overens" "Du vil være nødt til at logge ind igen og oprette en ny PIN-kode for at fortsætte." - "Du bliver logget ud" + "Denne enhed bliver fjernet" "Du har %1$d forsøg på at låse op" "Du har %1$d forsøg på at låse op" @@ -34,5 +34,5 @@ Vælg noget mindeværdigt. Hvis du glemmer denne pinkode, bliver du logget ud af "Brug biometri" "Brug PIN-kode" - "Logger ud…" + "Fjerner enhed…" diff --git a/features/lockscreen/impl/src/main/res/values-hu/translations.xml b/features/lockscreen/impl/src/main/res/values-hu/translations.xml index 3a65239697..470324b2ba 100644 --- a/features/lockscreen/impl/src/main/res/values-hu/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-hu/translations.xml @@ -23,7 +23,7 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele "Adja meg a PIN-kódját kétszer" "A PIN-kódok nem egyeznek" "A folytatáshoz újra be kell jelentkeznie, és létre kell hoznia egy új PIN-kódot" - "Kijelentkeztetésre kerül" + "Ez az eszköz eltávolításra kerül" "%1$d próbálkozása van a feloldáshoz" "%1$d próbálkozása van a feloldáshoz" @@ -34,5 +34,5 @@ Válasszon valami megjegyezhetőt. Ha elfelejti a PIN-kódot, akkor ki lesz jele "Biometrikus adatok használata" "PIN-kód használata" - "Kijelentkezés…" + "Eszköz eltávolítása…" diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml index 514d2461a2..5f9c82a34a 100644 --- a/features/lockscreen/impl/src/main/res/values-it/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml @@ -23,7 +23,7 @@ Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dall’a "Inserisci lo stesso PIN due volte" "I PIN non corrispondono" "Dovrai effettuare nuovamente l\'accesso e creare un nuovo PIN per procedere" - "Stai per essere disconnesso" + "Questo dispositivo verrà rimosso" "Hai %1$d tentativo di sblocco" "Hai %1$d tentativi di sblocco" @@ -34,5 +34,5 @@ Scegli un PIN facile da ricordare. Se lo dimentichi, verrai disconnesso dall’a "Usa la biometria" "Usa il PIN" - "Disconnessione in corso…" + "Rimozione del dispositivo…" diff --git a/features/lockscreen/impl/src/main/res/values-ja/translations.xml b/features/lockscreen/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..982c4574c8 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,36 @@ + + + "生体認証" + "生体認証で解除" + "生体認証を使用" + "生体認証を使用しますか?" + "PINをお忘れですか?" + "PINを変更" + "生体認証を使用" + "PINを削除" + "本当にPINを削除しますか?" + "PINを削除しますか?" + "%1$sを使用" + "PINを使用する" + "素早い認証のために %1$s を常に使用" + "PINを選択" + "PINの確認" + "チャットのセキュリティを強化するため、%1$s を保護しましょう。 + +覚えやすいPINを設定してください。PINを忘れると、アプリにログインできなくなります。" + "セキュリティ上の理由により、入力された内容をPINとして使用できません。" + "別のPINを使用してください" + "同一のPINを2回入力してください" + "PINが一致しません" + "再度ログインし、PINを再設定する必要があります" + "端末を削除しようとしています" + + "%1$d 回試すことができます" + + + "PINが間違っています。あと %1$d 回試すことができます。" + + "生体認証を使用" + "PINを使用" + "削除中…" + diff --git a/features/lockscreen/impl/src/main/res/values-ru/translations.xml b/features/lockscreen/impl/src/main/res/values-ru/translations.xml index d0441fb253..cb71dfc88c 100644 --- a/features/lockscreen/impl/src/main/res/values-ru/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml @@ -36,5 +36,5 @@ "Использовать биометрию" "Использовать PIN-код" - "Выполняется выход…" + "Удаление устройства…" diff --git a/features/lockscreen/impl/src/main/res/values-sv/translations.xml b/features/lockscreen/impl/src/main/res/values-sv/translations.xml index a559e4c909..021fa67fb3 100644 --- a/features/lockscreen/impl/src/main/res/values-sv/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sv/translations.xml @@ -34,5 +34,5 @@ Välj något minnesvärt. Om du glömmer den här PIN-koden loggas du ut från a "Använd biometri" "Använd PIN-kod" - "Loggar ut …" + "Tar bort enhet …" diff --git a/features/lockscreen/impl/src/main/res/values-vi/translations.xml b/features/lockscreen/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..2a177b843b --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,35 @@ + + + "xác thực sinh trắc học" + "mở khóa sinh trắc học" + "Mở khóa bằng sinh trắc học" + "Quên mã PIN rồi à?" + "Thay đổi mã PIN" + "Cho phép mở khóa bằng sinh trắc học" + "Xóa mã PIN" + "Bạn có chắc chắn muốn xóa mã PIN không?" + "Xóa mã PIN?" + "Cho phép %1$s" + "Tôi thích dùng mã PIN hơn." + "Dùng %1$s để mở khóa ứng dụng nhanh hơn." + "Chọn mã PIN" + "Xác nhận mã PIN" + "Khóa %1$s để tăng cường bảo mật cho các cuộc trò chuyện của bạn. + +Chọn một mã dễ nhớ. Nếu quên PIN này, bạn sẽ bị đăng xuất khỏi ứng dụng." + "Vì lý do bảo mật, bạn không thể chọn mã này làm mã PIN của mình." + "Chọn mã PIN khác" + "Vui lòng nhập cùng một mã PIN hai lần." + "Mã PIN không khớp" + "Đăng nhập lại và tạo PIN mới để tiếp tục." + "Thiết bị này đang được gỡ bỏ" + + "Bạn còn %1$d lần thử để mở khóa" + + + "PIN không đúng. Còn %1$d lần thử" + + "Sử dụng sinh trắc học" + "Sử dụng mã PIN" + "Đang gỡ thiết bị…" + diff --git a/features/lockscreen/impl/src/main/res/values-zh/translations.xml b/features/lockscreen/impl/src/main/res/values-zh/translations.xml index d3633e4af5..defe7a0e32 100644 --- a/features/lockscreen/impl/src/main/res/values-zh/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-zh/translations.xml @@ -32,5 +32,5 @@ "使用生物识别" "使用 PIN 码" - "正在登出…" + "正在删除设备……" diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index f19ba61783..12af922cbe 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -55,6 +55,7 @@ setupDependencyInjection() dependencies { implementation(projects.appconfig) implementation(projects.features.enterprise.api) + implementation(projects.features.preferences.api) implementation(projects.features.rageshake.api) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) @@ -79,6 +80,7 @@ dependencies { testCommonDependencies(libs, true) testImplementation(projects.features.login.test) testImplementation(projects.features.enterprise.test) + testImplementation(projects.features.preferences.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oidc.test) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 928d98c244..fb384d505a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -22,6 +22,7 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.singleTop import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted @@ -30,14 +31,17 @@ import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.ElementClassicConnection import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode +import io.element.android.features.login.impl.screens.classic.ClassicFlowNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.createaccount.CreateAccountNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode +import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode @@ -63,9 +67,11 @@ class LoginFlowNode( private val oidcActionFlow: OidcActionFlow, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val elementClassicConnection: ElementClassicConnection, + private val preferencesEntryPoint: PreferencesEntryPoint, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.OnBoarding, + initialElement = NavTarget.CheckClassicFlow, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -103,11 +109,19 @@ class LoginFlowNode( sealed interface NavTarget : Parcelable { @Parcelize - data object OnBoarding : NavTarget + data object CheckClassicFlow : NavTarget + + @Parcelize + data class OnBoarding( + val showBackButton: Boolean, + ) : NavTarget @Parcelize data object QrCode : NavTarget + @Parcelize + data object AppDeveloperSettings : NavTarget + @Parcelize data class ConfirmAccountProvider( val isAccountCreation: Boolean, @@ -123,7 +137,9 @@ class LoginFlowNode( data object SearchAccountProvider : NavTarget @Parcelize - data object LoginPassword : NavTarget + data class LoginPassword( + val initialLogin: String = "", + ) : NavTarget @Parcelize data class CreateAccount(val url: String) : NavTarget @@ -131,7 +147,31 @@ class LoginFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.OnBoarding -> { + NavTarget.CheckClassicFlow -> { + val callback = object : ClassicFlowNode.Callback { + override fun navigateToOnBoarding(allowBackNavigation: Boolean) { + if (allowBackNavigation) { + backstack.push(NavTarget.OnBoarding(showBackButton = true)) + } else { + backstack.replace(NavTarget.OnBoarding(showBackButton = false)) + } + } + + override fun navigateToLoginPassword() { + backstack.push(NavTarget.LoginPassword()) + } + + override fun navigateToOidc(oidcDetails: OidcDetails) { + navigateToMas(oidcDetails) + } + + override fun navigateToCreateAccount(url: String) { + backstack.push(NavTarget.CreateAccount(url)) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.OnBoarding -> { val callback = object : OnBoardingNode.Callback { override fun navigateToSignUpFlow() { backstack.push( @@ -165,21 +205,42 @@ class LoginFlowNode( backstack.push(NavTarget.CreateAccount(url)) } + override fun navigateToDeveloperSettings() { + backstack.push(NavTarget.AppDeveloperSettings) + } + override fun navigateToLoginPassword() { - backstack.push(NavTarget.LoginPassword) + backstack.push(NavTarget.LoginPassword()) } override fun onDone() { - callback.onDone() + if (navTarget.showBackButton) { + backstack.pop() + } else { + callback.onDone() + } } } val params = inputs() val inputs = OnBoardingNode.Params( accountProvider = params.accountProvider, loginHint = params.loginHint, + showBackButton = navTarget.showBackButton, ) createNode(buildContext, listOf(callback, inputs)) } + NavTarget.AppDeveloperSettings -> { + val callback = object : PreferencesEntryPoint.DeveloperSettingsCallback { + override fun onDone() { + backstack.pop() + } + } + preferencesEntryPoint.createAppDeveloperSettingsNode( + parentNode = this, + buildContext = buildContext, + callback = callback, + ) + } NavTarget.ChooseAccountProvider -> { val callback = object : ChooseAccountProviderNode.Callback { override fun navigateToOidc(oidcDetails: OidcDetails) { @@ -191,7 +252,7 @@ class LoginFlowNode( } override fun navigateToLoginPassword() { - backstack.push(NavTarget.LoginPassword) + backstack.push(NavTarget.LoginPassword()) } } createNode(buildContext, listOf(callback)) @@ -218,7 +279,7 @@ class LoginFlowNode( } override fun navigateToLoginPassword() { - backstack.push(NavTarget.LoginPassword) + backstack.push(NavTarget.LoginPassword()) } override fun navigateToChangeAccountProvider() { @@ -257,8 +318,11 @@ class LoginFlowNode( createNode(buildContext, plugins = listOf(callback)) } - NavTarget.LoginPassword -> { - createNode(buildContext) + is NavTarget.LoginPassword -> { + val inputs = LoginPasswordNode.Inputs( + initialLogin = navTarget.initialLogin, + ) + createNode(buildContext, plugins = listOf(inputs)) } is NavTarget.CreateAccount -> { val inputs = CreateAccountNode.Inputs( @@ -280,6 +344,14 @@ class LoginFlowNode( override fun View(modifier: Modifier) { activity = requireNotNull(LocalActivity.current) darkTheme = !ElementTheme.isLightTheme + + DisposableEffect(Unit) { + elementClassicConnection.start() + onDispose { + elementClassicConnection.stop() + } + } + DisposableEffect(Unit) { onDispose { activity = null @@ -288,6 +360,6 @@ class LoginFlowNode( } } } - BackstackView() + BackstackView(transitionHandler = rememberLoginFlowTransitionHandler()) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowTransitionHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowTransitionHandler.kt new file mode 100644 index 0000000000..5486619e5d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowTransitionHandler.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler +import com.bumble.appyx.core.navigation.transition.TransitionDescriptor +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.Replace +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider +import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler + +/** + * A TransitionHandler that uses fade transition when OnBoarding is replacing the current screen, + * and slide transition for all other cases. + */ +private class LoginFlowTransitionHandler( + private val slider: ModifierTransitionHandler, + private val fader: ModifierTransitionHandler, +) : ModifierTransitionHandler() { + override fun createModifier( + modifier: Modifier, + transition: Transition, + descriptor: TransitionDescriptor + ): Modifier { + val useFader = descriptor.element is LoginFlowNode.NavTarget.OnBoarding && + descriptor.operation is Replace + val handler = if (useFader) fader else slider + return handler.createModifier(modifier, transition, descriptor) + } +} + +@Composable +fun rememberLoginFlowTransitionHandler(): ModifierTransitionHandler { + val slider = rememberBackstackSlider( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + val fader = rememberBackstackFader( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + return rememberDelegateTransitionHandler { + LoginFlowTransitionHandler(slider, fader) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/classic/ElementClassicConnection.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/classic/ElementClassicConnection.kt new file mode 100644 index 0000000000..c928c05239 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/classic/ElementClassicConnection.kt @@ -0,0 +1,423 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.classic + +import android.content.ComponentName +import android.content.Context.BIND_AUTO_CREATE +import android.content.Intent +import android.content.ServiceConnection +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import androidx.annotation.VisibleForTesting +import androidx.core.os.BundleCompat +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn +import io.element.android.features.login.impl.BuildConfig +import io.element.android.libraries.androidutils.service.ServiceBinder +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.di.annotations.AppCoroutineScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.auth.ElementClassicSession +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +interface ElementClassicConnection { + fun start() + fun stop() + fun requestSession() + val stateFlow: StateFlow +} + +sealed interface ElementClassicConnectionState { + object Idle : ElementClassicConnectionState + object ElementClassicNotFound : ElementClassicConnectionState + object ElementClassicReadyNoSession : ElementClassicConnectionState + data class ElementClassicReady( + val elementClassicSession: ElementClassicSession, + val displayName: String?, + val avatar: Bitmap?, + ) : ElementClassicConnectionState + + data class Error(val error: String) : ElementClassicConnectionState +} + +private val loggerTag = LoggerTag("ECConnection") + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class DefaultElementClassicConnection( + private val serviceBinder: ServiceBinder, + @AppCoroutineScope + private val coroutineScope: CoroutineScope, + private val matrixAuthenticationService: MatrixAuthenticationService, + private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, + private val featureFlagService: FeatureFlagService, +) : ElementClassicConnection { + // Messenger for communicating with the service. + private var messenger: Messenger? = null + + // Target we publish for external service to send messages to IncomingHandler. + private val incomingMessenger: Messenger = Messenger(IncomingHandler()) + + // Flag indicating whether we have called bind on the service. + private var bound: Boolean = false + + private val mutableStateFlow = MutableStateFlow(ElementClassicConnectionState.Idle) + override val stateFlow = mutableStateFlow.asStateFlow() + + private val elementClassicComponent = ComponentName( + BuildConfig.elementClassicPackage, + ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME, + ) + + /** + * Class for interacting with the main interface of the service. + */ + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + Timber.tag(loggerTag.value).d("onServiceConnected") + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + messenger = Messenger(service) + bound = true + // Request the data as soon as possible + requestSession() + } + + override fun onServiceDisconnected(className: ComponentName) { + Timber.tag(loggerTag.value).d("onServiceDisconnected") + // This is called when the connection with the service has been + // unexpectedly disconnected—that is, its process crashed. + messenger = null + bound = false + } + } + + override fun start() { + Timber.tag(loggerTag.value).d("start()") + coroutineScope.launch { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SignInWithClassic)) { + Timber.tag(loggerTag.value).d("Login with Element Classic is disabled, not starting connection") + return@launch + } + // Establish a connection with the service. We use an explicit + // class name because there is no reason to be able to let other + // applications replace our component. + try { + val intentService = Intent() + intentService.setComponent(elementClassicComponent) + if (serviceBinder.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) { + Timber.tag(loggerTag.value).d("Binding returned true") + } else { + // This happens when the app is not installed + Timber.tag(loggerTag.value).d("Binding returned false") + emitState(ElementClassicConnectionState.ElementClassicNotFound) + } + } catch (e: SecurityException) { + Timber.tag(loggerTag.value).e(e, "Can't bind to Service") + emitState(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) + } + } + } + + override fun stop() { + Timber.tag(loggerTag.value).d("stop(): Unbinding (bound=$bound)") + if (bound) { + // Detach our existing connection. + serviceBinder.unbindService(serviceConnection) + bound = false + } + coroutineScope.launch { + emitState(ElementClassicConnectionState.Idle) + } + } + + override fun requestSession() { + Timber.tag(loggerTag.value).d("requestSession()") + coroutineScope.launch { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SignInWithClassic)) { + Timber.tag(loggerTag.value).d("Login with Element Classic is disabled") + emitState(ElementClassicConnectionState.Error("The feature is disabled")) + return@launch + } + val finalMessenger = messenger + if (finalMessenger == null) { + Timber.tag(loggerTag.value).d("The messenger is null, can't request data") + // Do not emit error, else the regular on boarding flow will be displayed + } else { + try { + // Get the data + val msg = Message.obtain(null, MSG_GET_SESSION) + msg.replyTo = incomingMessenger + finalMessenger.send(msg) + } catch (e: RemoteException) { + // In this case the service has crashed before we could even + // do anything with it; we can count on soon being + // disconnected (and then reconnected if it can be restarted) + // so there is no need to do anything here. + Timber.tag(loggerTag.value).e(e, "RemoteException") + emitState(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) + } + } + } + } + + private fun requestAvatar(userId: UserId) { + Timber.tag(loggerTag.value).d("requestAvatar()") + coroutineScope.launch { + val finalMessenger = messenger + if (finalMessenger == null) { + Timber.tag(loggerTag.value).w("The messenger is null, can't request extra data") + } else { + try { + // Get the data + val msg = Message.obtain(null, MSG_GET_AVATAR) + msg.data = Bundle().apply { + putString(KEY_USER_ID_STR, userId.value) + } + msg.replyTo = incomingMessenger + finalMessenger.send(msg) + } catch (e: RemoteException) { + // In this case the service has crashed before we could even + // do anything with it; we can count on soon being + // disconnected (and then reconnected if it can be restarted) + // so there is no need to do anything here. + Timber.tag(loggerTag.value).e(e, "RemoteException") + } + } + } + } + + /** + * Handler of incoming messages from service. + */ + @Suppress("DEPRECATION") + inner class IncomingHandler : Handler() { + override fun handleMessage(msg: Message) { + Timber.tag(loggerTag.value).d("IncomingHandler handling message ${msg.what}") + when (msg.what) { + MSG_GET_SESSION -> onSessionReceived(msg.data) + MSG_GET_AVATAR -> onAvatarReceived(msg.data) + else -> { + Timber.tag(loggerTag.value).w("Received unknown message ${msg.what}") + super.handleMessage(msg) + } + } + } + } + + @VisibleForTesting + fun onSessionReceived(data: Bundle) { + // The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied + val state = data.toElementClassicConnectionState() + coroutineScope.launch { + val updatedState = ensureHomeserverIsSupported(state) + emitState(updatedState) + val userId = (updatedState as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession?.userId + if (userId != null) { + // Step 2, request the avatar + requestAvatar(userId) + } + } + } + + @VisibleForTesting + fun onAvatarReceived(data: Bundle) { + val currentState = stateFlow.value + if (currentState is ElementClassicConnectionState.ElementClassicReady) { + // Check that the userId is still the same + val userId = data.getString(KEY_USER_ID_STR) + if (userId != currentState.elementClassicSession.userId.value) { + Timber.tag(loggerTag.value).w( + "Received profile data for userId $userId but current" + + " userId is ${currentState.elementClassicSession.userId}, ignoring" + ) + } else { + val avatar = BundleCompat.getParcelable(data, KEY_USER_AVATAR_PARCELABLE, Bitmap::class.java) + // If the avatar is identical to the current one, do not emit a new state to avoid unnecessary recompositions + // and blink on the avatar image + if (avatar == null || !avatar.sameAs(currentState.avatar)) { + val updatedState = currentState.copy( + avatar = avatar, + ) + coroutineScope.launch { + emitState(updatedState) + } + } + } + } else { + Timber.tag(loggerTag.value).w("Received profile data but current state is not ElementClassicReady: %s", currentState) + } + } + + private suspend fun ensureHomeserverIsSupported(state: ElementClassicConnectionState): ElementClassicConnectionState { + return if (state is ElementClassicConnectionState.ElementClassicReady) { + val elementXCanConnect = setOfNotNull( + // Try with the domain name first + state.elementClassicSession.userId.domainName?.ensureProtocol(), + // Then try with the resolved homeserver URL, if provided and distinct + state.elementClassicSession.homeserverUrl, + ).any { url -> + val isCompatible = homeServerLoginCompatibilityChecker.check(url) + .onFailure { + Timber.tag(loggerTag.value).w(it, "Failed to check compatibility with homeserver: $url") + } + .getOrNull() == true + if (isCompatible) { + Timber.tag(loggerTag.value).d("Found compatible homeserver URL: %s", url) + } else { + Timber.tag(loggerTag.value).d("Homeserver URL is not compatible: %s", url) + } + isCompatible + } + if (elementXCanConnect) { + state + } else { + Timber.tag(loggerTag.value).w("Cannot import session because the homeserver is not compatible with Element X") + ElementClassicConnectionState.Error("The homeserver is not compatible with Element X") + } + } else { + state + } + } + + private suspend fun emitState(state: ElementClassicConnectionState) { + when (state) { + is ElementClassicConnectionState.Error -> { + Timber.tag(loggerTag.value).w("Error: %s", state.error) + } + is ElementClassicConnectionState.ElementClassicReady -> { + Timber.tag(loggerTag.value).d("Ready state for user: %s", state.elementClassicSession.userId) + } + ElementClassicConnectionState.ElementClassicReadyNoSession -> { + Timber.tag(loggerTag.value).d("No session from Element Classic") + } + ElementClassicConnectionState.ElementClassicNotFound -> { + Timber.tag(loggerTag.value).d("Element Classic not found") + } + ElementClassicConnectionState.Idle -> { + Timber.tag(loggerTag.value).d("Idle") + } + } + // Also give the Element Classic session info to the MatrixAuthenticationService + matrixAuthenticationService.setElementClassicSession( + session = (state as? ElementClassicConnectionState.ElementClassicReady)?.elementClassicSession + ) + mutableStateFlow.emit(state) + } + + private fun Bundle.toElementClassicConnectionState(): ElementClassicConnectionState { + val error = getString(KEY_ERROR_STR) + return if (error != null) { + ElementClassicConnectionState.Error(error) + } else { + val userId = getString(KEY_USER_ID_STR)?.takeIf { it.isNotEmpty() }?.let(::UserId) + if (userId == null) { + ElementClassicConnectionState.ElementClassicReadyNoSession + } else { + var secrets = getString(KEY_SECRETS_STR)?.takeIf { it.isNotEmpty() } + val roomKeysVersion = getString(KEY_ROOM_KEYS_VERSION_STR) + .also { + if (secrets != null && it == null) { + Timber.tag(loggerTag.value).w("Room keys version is null, outdated version of Element Classic, ignore secrets") + // In this case, just ignore the secrets, the SDK will not accept them anyway + secrets = null + } + } + ?.takeIf { it.isNotEmpty() } + val homeserverUrl = getString(KEY_HOMESERVER_URL_STR)?.takeIf { it.isNotEmpty() } + val displayName = getString(KEY_USER_DISPLAY_NAME_STR)?.takeIf { it.isNotEmpty() } + val doesContainBackupKey = secrets != null && + roomKeysVersion != null && + matrixAuthenticationService.doSecretsContainBackupKey(userId, secrets, roomKeysVersion) + Timber.tag(loggerTag.value).d( + buildString { + append("Receiving session $userId ($displayName) from Element Classic, with secrets: ") + append(secrets != null) + append(", with roomKeysVersion: ") + append(roomKeysVersion != null) + append(", with valid backup key: ") + append(doesContainBackupKey) + } + ) + // Ensure avatar is not lost when refreshing the data + val currentAvatar = (stateFlow.value as? ElementClassicConnectionState.ElementClassicReady) + ?.takeIf { it.elementClassicSession.userId == userId } + ?.avatar + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = userId, + homeserverUrl = homeserverUrl, + secrets = secrets, + roomKeysVersion = roomKeysVersion, + doesContainBackupKey = doesContainBackupKey, + ), + displayName = displayName, + avatar = currentAvatar, + ) + } + } + } + + // Everything in this companion object must match what is defined in Element Classic + companion object { + const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService" + + // Command to the service to get the userId/displayName/secrets of a verified session. + const val MSG_GET_SESSION = 1 + + // Command to the service to get the avatar oor the session. + const val MSG_GET_AVATAR = 2 + + // Keys for the bundle returned from the service + const val KEY_ERROR_STR = "error" + const val KEY_USER_ID_STR = "userId" + const val KEY_HOMESERVER_URL_STR = "homeserverUrl" + const val KEY_USER_DISPLAY_NAME_STR = "displayName" + + /** + * Key to extract the secrets from the bundle, as a Json string. + * Json will have this format: + * { + * "cross_signing" : { + * "master_key" : "z8RUxnaAGu___REDACTED___k+BQL9o", + * "user_signing_key" : "baJHzA___REDACTED___xMLbSUAXw9QUzqms", + * "self_signing_key" : "DU0CvLtR2G/___REDACTED___dV/MONNq4nsQhM" + * }, + * "backup" : { + * "algorithm" : "m.megolm_backup.v1.curve25519-aes-sha2", + * "key" : "VzncmQ+UOV___REDACTED___patxDz7m0Nc", + * "backup_version" : "1" + * } + * } + */ + const val KEY_SECRETS_STR = "secrets" + const val KEY_ROOM_KEYS_VERSION_STR = "roomKeysVersion" + + // For the avatar + const val KEY_USER_AVATAR_PARCELABLE = "avatar" + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt index 12b9106b71..4523e6f45e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -14,8 +14,6 @@ import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerState -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicPresenter -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @@ -23,7 +21,4 @@ import io.element.android.libraries.architecture.Presenter interface LoginModule { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter - - @Binds - fun bindLoginWithClassicPresenter(presenter: LoginWithClassicPresenter): Presenter } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt index a62919e705..78be770bfc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt @@ -60,10 +60,19 @@ class LoginHelper( suspend fun submit( isAccountCreation: Boolean, homeserverUrl: String, + resolvedHomeserverUrl: String?, loginHint: String?, ) { suspend { - authenticationService.setHomeserver(homeserverUrl).map { matrixHomeServerDetails -> + authenticationService.setHomeserver(homeserverUrl).recoverCatching { + // No .well-known file? + // If the homeserver is not reachable, try using resolvedHomeserverUrl. + if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) { + authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow() + } else { + throw it + } + }.map { matrixHomeServerDetails -> if (matrixHomeServerDetails.supportsOidcLogin) { // Retrieve the details right now val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index 87010a4a30..c6d6d76486 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -44,6 +44,7 @@ class ChooseAccountProviderPresenter( loginHelper.submit( isAccountCreation = false, homeserverUrl = it.url, + resolvedHomeserverUrl = null, loginHint = null, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNode.kt new file mode 100644 index 0000000000..f2ff998652 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNode.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.lifecycle.subscribe +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.newRoot +import com.bumble.appyx.navmodel.backstack.operation.pop +import com.bumble.appyx.navmodel.backstack.operation.push +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicNode +import io.element.android.features.login.impl.screens.classic.missingkeybackup.MissingKeyBackupNode +import io.element.android.features.login.impl.screens.classic.root.RootNode +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.appyx.rememberFaderOrSliderTransitionHandler +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +@AssistedInject +class ClassicFlowNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val classicFlowNodeHelper: ClassicFlowNodeHelper, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + interface Callback : Plugin { + fun navigateToOnBoarding(allowBackNavigation: Boolean) + fun navigateToLoginPassword() + fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToCreateAccount(url: String) + } + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data class LoginWithClassic( + val userId: UserId, + ) : NavTarget + + @Parcelize + data object MissingKeyBackup : NavTarget + } + + private val callback: Callback = callback() + + override fun onBuilt() { + super.onBuilt() + observeElementClassicConnection() + lifecycle.subscribe( + onResume = { + classicFlowNodeHelper.onResume() + }, + ) + } + + private fun observeElementClassicConnection() { + classicFlowNodeHelper.navigationEventFlow().onEach { navigationEvent -> + when (navigationEvent) { + is NavigationEvent.Idle -> Unit + is NavigationEvent.NavigateToOnBoarding -> callback.navigateToOnBoarding(allowBackNavigation = false) + is NavigationEvent.NavigateToLoginWithClassic -> backstack.newRoot(NavTarget.LoginWithClassic(navigationEvent.userId)) + } + }.launchIn(lifecycleScope) + } + + override fun resolve( + navTarget: NavTarget, + buildContext: BuildContext, + ): Node { + return when (navTarget) { + NavTarget.Root -> { + createNode(buildContext) + } + is NavTarget.LoginWithClassic -> { + val callback = object : LoginWithClassicNode.Callback { + override fun navigateToOtherOptions() { + callback.navigateToOnBoarding(allowBackNavigation = true) + } + + override fun navigateToLoginPassword() { + callback.navigateToLoginPassword() + } + + override fun navigateToOidc(oidcDetails: OidcDetails) { + callback.navigateToOidc(oidcDetails) + } + + override fun navigateToCreateAccount(url: String) { + callback.navigateToCreateAccount(url) + } + + override fun navigateToMissingKeyBackup() { + backstack.push(NavTarget.MissingKeyBackup) + } + } + val inputs = LoginWithClassicNode.Inputs( + userId = navTarget.userId, + ) + createNode(buildContext, plugins = listOf(inputs, callback)) + } + NavTarget.MissingKeyBackup -> { + val callback = object : MissingKeyBackupNode.Callback { + override fun navigateBack() { + backstack.pop() + } + } + createNode(buildContext, listOf(callback)) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView( + modifier = modifier, + transitionHandler = rememberFaderOrSliderTransitionHandler(), + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelper.kt new file mode 100644 index 0000000000..a5bc74c5e4 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelper.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic + +import dev.zacsweers.metro.Inject +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserListFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf + +@Inject +class ClassicFlowNodeHelper( + private val elementClassicConnection: ElementClassicConnection, + private val sessionStore: SessionStore, +) { + fun onResume() { + elementClassicConnection.requestSession() + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun navigationEventFlow(): Flow { + return elementClassicConnection.stateFlow + .distinctUntilChangedBy { + // Ignore change on ElementClassicConnectionState.ElementClassicReady.avatar + if (it is ElementClassicConnectionState.ElementClassicReady) { + it.copy(avatar = null) + } else { + it + } + } + .flatMapLatest { elementClassicConnectionState -> + when (elementClassicConnectionState) { + ElementClassicConnectionState.Idle -> { + // Ensure user is not stuck on the loading screen. + // If Element Classic is taking too long to communicate (or crashes), unblock the user after a few seconds. + flow { + emit(NavigationEvent.Idle) + delay(5_000) + emit(NavigationEvent.NavigateToOnBoarding) + } + } + ElementClassicConnectionState.ElementClassicNotFound, + ElementClassicConnectionState.ElementClassicReadyNoSession, + is ElementClassicConnectionState.Error -> { + flowOf(NavigationEvent.NavigateToOnBoarding) + } + is ElementClassicConnectionState.ElementClassicReady -> { + val existingSessions = sessionStore.sessionsFlow().toUserListFlow().first() + if (elementClassicConnectionState.elementClassicSession.userId.value in existingSessions) { + flowOf(NavigationEvent.NavigateToOnBoarding) + } else { + // 2 cases when this can be run: + // First time this screen will be displayed + // Missing key backup screen was displayed, but the data has changed (user set up the key backup on Classic), + // and the app is resuming. + flowOf(NavigationEvent.NavigateToLoginWithClassic(elementClassicConnectionState.elementClassicSession.userId)) + } + } + } + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/NavigationEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/NavigationEvent.kt new file mode 100644 index 0000000000..cddca0015b --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/NavigationEvent.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic + +import io.element.android.libraries.matrix.api.core.UserId + +sealed interface NavigationEvent { + data object Idle : NavigationEvent + data object NavigateToOnBoarding : NavigationEvent + data class NavigateToLoginWithClassic( + val userId: UserId, + ) : NavigationEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicEvent.kt new file mode 100644 index 0000000000..6ba9b2142a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicEvent.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +sealed interface LoginWithClassicEvent { + data object Submit : LoginWithClassicEvent + data object ClearError : LoginWithClassicEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNavigator.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNavigator.kt new file mode 100644 index 0000000000..55716c2cf7 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNavigator.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +interface LoginWithClassicNavigator { + fun navigateToMissingKeyBackup() +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNode.kt new file mode 100644 index 0000000000..c42248a3f8 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicNode.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.core.UserId + +@ContributesNode(AppScope::class) +@AssistedInject +class LoginWithClassicNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: LoginWithClassicPresenter.Factory, +) : Node(buildContext, plugins = plugins), + LoginWithClassicNavigator { + interface Callback : Plugin { + fun navigateToOtherOptions() + fun navigateToLoginPassword() + fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToCreateAccount(url: String) + fun navigateToMissingKeyBackup() + } + + data class Inputs( + val userId: UserId, + ) : NodeInputs + + private val inputs: Inputs = inputs() + val presenter = presenterFactory.create(inputs.userId, this) + private val callback: Callback = callback() + + override fun navigateToMissingKeyBackup() { + callback.navigateToMissingKeyBackup() + } + + @Composable + override fun View(modifier: Modifier) { + val context = LocalContext.current + val state = presenter.present() + LoginWithClassicView( + state = state, + modifier = modifier, + onOtherOptionsClick = callback::navigateToOtherOptions, + onOidcDetails = callback::navigateToOidc, + onNeedLoginPassword = callback::navigateToLoginPassword, + onLearnMoreClick = { openLearnMorePage(context) }, + onCreateAccountContinue = callback::navigateToCreateAccount, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt new file mode 100644 index 0000000000..90a528c3ae --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.coroutines.launch + +@AssistedInject +class LoginWithClassicPresenter( + @Assisted private val userId: UserId, + @Assisted private val navigator: LoginWithClassicNavigator, + private val loginHelper: LoginHelper, + private val elementClassicConnection: ElementClassicConnection, + private val accountProviderDataSource: AccountProviderDataSource, + private val buildMeta: BuildMeta, +) : Presenter { + @AssistedFactory + interface Factory { + fun create( + userId: UserId, + navigator: LoginWithClassicNavigator, + ): LoginWithClassicPresenter + } + + @Composable + override fun present(): LoginWithClassicState { + val coroutineScope = rememberCoroutineScope() + var loginWithClassicAction by remember { + mutableStateOf>(AsyncAction.Uninitialized) + } + val loginMode by loginHelper.collectLoginMode() + val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState() + + fun handleEvent(event: LoginWithClassicEvent) { + when (event) { + LoginWithClassicEvent.Submit -> { + val currentState = elementClassicConnection.stateFlow.value + if (currentState is ElementClassicConnectionState.ElementClassicReady) { + if (currentState.elementClassicSession.secrets != null && + !currentState.elementClassicSession.doesContainBackupKey) { + navigator.navigateToMissingKeyBackup() + } else { + coroutineScope.launch { + loginWithClassicAction = AsyncAction.Loading + // Ensure that the current account provider is set + val elementClassicUserId = currentState.elementClassicSession.userId + val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol() + accountProviderDataSource.setUrl(accountProvider) + loginHelper.submit( + isAccountCreation = false, + homeserverUrl = accountProvider, + resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, + loginHint = "mxid:" + elementClassicUserId.value, + ) + } + } + } else { + loginWithClassicAction = AsyncAction.Failure(IllegalStateException("Element Classic is not ready")) + } + } + LoginWithClassicEvent.ClearError -> { + loginWithClassicAction = AsyncAction.Uninitialized + loginHelper.clearError() + } + } + } + + val elementClassicReady = elementClassicConnectionState as? ElementClassicConnectionState.ElementClassicReady + return LoginWithClassicState( + isElementPro = buildMeta.isEnterpriseBuild, + userId = userId, + displayName = elementClassicReady?.displayName, + avatar = elementClassicReady?.avatar, + loginMode = loginMode, + loginWithClassicAction = loginWithClassicAction, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt new file mode 100644 index 0000000000..275a444768 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import android.graphics.Bitmap +import androidx.compose.runtime.Stable +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId + +@Stable +data class LoginWithClassicState( + val isElementPro: Boolean, + val userId: UserId, + val displayName: String?, + val avatar: Bitmap?, + val loginWithClassicAction: AsyncAction, + val loginMode: AsyncData, + val eventSink: (LoginWithClassicEvent) -> Unit, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt new file mode 100644 index 0000000000..d8dcfeb072 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import android.graphics.Bitmap +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.login.LoginMode +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.UserId + +open class LoginWithClassicStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLoginWithClassicState(), + aLoginWithClassicState(isElementPro = true, displayName = "Alice"), + ) +} + +fun aLoginWithClassicState( + isElementPro: Boolean = false, + userId: UserId = UserId("@alice:matrix.org"), + displayName: String? = null, + avatar: Bitmap? = null, + loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, + loginMode: AsyncData = AsyncData.Uninitialized, + eventSink: (LoginWithClassicEvent) -> Unit = {}, +) = LoginWithClassicState( + isElementPro = isElementPro, + userId = userId, + displayName = displayName, + avatar = avatar, + loginWithClassicAction = loginWithClassicAction, + loginMode = loginMode, + eventSink = eventSink, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt new file mode 100644 index 0000000000..6b5c48f1ec --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.login.LoginModeView +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.background.OnboardingBackground +import io.element.android.libraries.designsystem.components.async.AsyncActionView +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.BitmapAvatar +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginWithClassicView( + state: LoginWithClassicState, + onOtherOptionsClick: () -> Unit, + onOidcDetails: (OidcDetails) -> Unit, + onNeedLoginPassword: () -> Unit, + onLearnMoreClick: () -> Unit, + onCreateAccountContinue: (url: String) -> Unit, + modifier: Modifier = Modifier, +) { + val isLoading by remember(state.loginMode) { + derivedStateOf { + state.loginMode is AsyncData.Loading + } + } + + HeaderFooterPage( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + background = { OnboardingBackground() }, + isScrollable = true, + header = { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(40.dp)) + Box( + modifier = Modifier + .size(54.dp) + .shadow(elevation = 10.dp, shape = RoundedCornerShape(15.dp)) + .background(ElementTheme.colors.bgCanvasDefault, shape = RoundedCornerShape(15.dp)), + contentAlignment = Alignment.Center, + ) { + val resId = if (state.isElementPro) { + R.drawable.element_pro_logo + } else { + R.drawable.element_foss_logo + } + Image( + modifier = Modifier.size(37.5.dp), + painter = painterResource(id = resId), + contentDescription = null, + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.screen_onboarding_welcome_title), + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingMdBold, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(10.dp)) + } + }, + content = { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(40.dp)) + BitmapAvatar( + avatarData = AvatarData( + id = state.userId.value, + name = state.displayName, + // Not used here + url = null, + size = AvatarSize.UserHeader, + ), + bitmap = state.avatar, + ) + Spacer(Modifier.height(24.dp)) + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResource(R.string.screen_onboarding_welcome_back), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + // User display name + if (state.displayName != null) { + Text( + text = state.displayName, + style = ElementTheme.typography.fontHeadingLgBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(Modifier.height(16.dp)) + } + // UserId + Text( + text = state.userId.value, + style = if (state.displayName == null) ElementTheme.typography.fontHeadingLgBold else ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + // Min spacing + Spacer(Modifier.height(45.dp)) + ButtonColumnMolecule { + Button( + text = stringResource(CommonStrings.action_continue), + showProgress = isLoading, + onClick = { + state.eventSink(LoginWithClassicEvent.Submit) + }, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + ) + OutlinedButton( + text = stringResource(CommonStrings.common_other_options), + onClick = onOtherOptionsClick, + enabled = !isLoading, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + ) + } + } + }, + footer = {}, + ) + + AsyncActionView( + async = state.loginWithClassicAction, + onErrorDismiss = { + state.eventSink(LoginWithClassicEvent.ClearError) + }, + onSuccess = { + // noop, the view will be closed + }, + progressDialog = { + // The button is showing the progress + } + ) + LoginModeView( + loginMode = state.loginMode, + onClearError = { + state.eventSink(LoginWithClassicEvent.ClearError) + }, + onLearnMoreClick = onLearnMoreClick, + onOidcDetails = onOidcDetails, + onNeedLoginPassword = onNeedLoginPassword, + onCreateAccountContinue = onCreateAccountContinue, + ) +} + +@PreviewsDayNight +@Composable +internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicStateProvider::class) state: LoginWithClassicState) = ElementPreview { + LoginWithClassicView( + state = state, + onOtherOptionsClick = {}, + onOidcDetails = {}, + onNeedLoginPassword = {}, + onLearnMoreClick = {}, + onCreateAccountContinue = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupNode.kt new file mode 100644 index 0000000000..45c16e7cde --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupNode.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.missingkeybackup + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.login.impl.BuildConfig +import io.element.android.libraries.architecture.callback +import timber.log.Timber + +@ContributesNode(AppScope::class) +@AssistedInject +class MissingKeyBackupNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: MissingKeyBackupPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun navigateBack() + } + + private val callback: Callback = callback() + + /** + * Open Element Classic application. + */ + private fun openClassic(context: Context) { + context.packageManager.getLaunchIntentForPackage( + BuildConfig.elementClassicPackage, + )?.let { intent -> + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + // Should not happen, Element Classic must be installed for this screen to be displayed. + Timber.e(e, "Element Classic app not found, cannot open it.") + } + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + MissingKeyBackupView( + state = state, + onBackClick = callback::navigateBack, + onOpenClassicClick = { + openClassic(context) + }, + modifier = modifier, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenter.kt new file mode 100644 index 0000000000..593c50dcb5 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.missingkeybackup + +import androidx.compose.runtime.Composable +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta + +@Inject +class MissingKeyBackupPresenter( + private val buildMeta: BuildMeta, +) : Presenter { + @Composable + override fun present(): MissingKeyBackupState { + return MissingKeyBackupState( + appName = buildMeta.applicationName, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupState.kt new file mode 100644 index 0000000000..31eaf015a0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupState.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.missingkeybackup + +data class MissingKeyBackupState( + val appName: String, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt new file mode 100644 index 0000000000..2c6a09b3ed --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.missingkeybackup + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class MissingKeyBackupStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMissingKeyBackupState(), + // Add other state here + ) +} + +fun aMissingKeyBackupState( + appName: String = "AppName", +) = MissingKeyBackupState( + appName = appName, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt new file mode 100644 index 0000000000..c4c9c5f286 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.missingkeybackup + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism +import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun MissingKeyBackupView( + state: MissingKeyBackupState, + onBackClick: () -> Unit, + onOpenClassicClick: () -> Unit, + modifier: Modifier = Modifier, +) { + FlowStepPage( + modifier = modifier, + onBackClick = onBackClick, + iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()), + title = stringResource(id = R.string.screen_missing_key_backup_title, state.appName), + content = { Content(state) }, + buttons = { + Buttons( + onOpenClassicClick = onOpenClassicClick, + ) + } + ) +} + +@Composable +private fun Content( + state: MissingKeyBackupState, +) { + NumberedListOrganism( + modifier = Modifier.padding(top = 50.dp, start = 20.dp, end = 20.dp), + items = persistentListOf( + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_1)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_2_android)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_3_android)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_4)), + AnnotatedString(stringResource(R.string.screen_missing_key_backup_step_5, state.appName)), + ), + ) +} + +@Composable +private fun ColumnScope.Buttons( + onOpenClassicClick: () -> Unit, +) { + Button( + text = stringResource(id = R.string.screen_missing_key_backup_open_element_classic), + modifier = Modifier.fillMaxWidth(), + onClick = onOpenClassicClick, + ) +} + +@PreviewsDayNight +@Composable +internal fun MissingKeyBackupViewPreview(@PreviewParameter(MissingKeyBackupStateProvider::class) state: MissingKeyBackupState) = ElementPreview { + MissingKeyBackupView( + state = state, + onBackClick = {}, + onOpenClassicClick = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootNode.kt new file mode 100644 index 0000000000..adb8c2d728 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootNode.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.root + +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 dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode + +@ContributesNode(AppScope::class) +@AssistedInject +class RootNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + RootView(modifier) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootView.kt new file mode 100644 index 0000000000..f1ca4b048a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/root/RootView.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.root + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.utils.DelayedVisibility +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun RootView( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + DelayedVisibility( + duration = 100.milliseconds, + ) { + CircularProgressIndicator() + } + } +} + +@PreviewsDayNight +@Composable +internal fun RootViewPreview() = ElementPreview { + RootView() +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index c38da7b11c..bf06613830 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -48,6 +48,7 @@ class ConfirmAccountProviderPresenter( loginHelper.submit( isAccountCreation = params.isAccountCreation, homeserverUrl = accountProvider.url, + resolvedHomeserverUrl = null, loginHint = null, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt index c6ce16141d..853b8a7423 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt @@ -17,14 +17,23 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs @ContributesNode(AppScope::class) @AssistedInject class LoginPasswordNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: LoginPasswordPresenter, + presenterFactory: LoginPasswordPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + data class Inputs( + val initialLogin: String, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.initialLogin) + @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt index b1ddc6e5b8..f26f342a42 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt @@ -16,7 +16,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -25,11 +27,18 @@ import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -@Inject +@AssistedInject class LoginPasswordPresenter( + @Assisted + private val initialLogin: String, private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(initialLogin: String): LoginPasswordPresenter + } + @Composable override fun present(): LoginPasswordState { val localCoroutineScope = rememberCoroutineScope() @@ -38,7 +47,12 @@ class LoginPasswordPresenter( } val formState = rememberSaveable { - mutableStateOf(LoginFormState.Default) + mutableStateOf( + LoginFormState( + login = initialLogin, + password = "", + ) + ) } val accountProvider by accountProviderDataSource.flow.collectAsState() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt index 1ded677c13..5572c412a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingNode.kt @@ -42,12 +42,14 @@ class OnBoardingNode( fun navigateToLoginPassword() fun navigateToOidc(oidcDetails: OidcDetails) fun navigateToCreateAccount(url: String) + fun navigateToDeveloperSettings() fun onDone() } data class Params( val accountProvider: String?, val loginHint: String?, + val showBackButton: Boolean, ) : NodeInputs private val callback: Callback = callback() @@ -61,6 +63,7 @@ class OnBoardingNode( override fun View(modifier: Modifier) { val state = presenter.present() val context = LocalContext.current + OnBoardingView( state = state, modifier = modifier, @@ -73,6 +76,7 @@ class OnBoardingNode( onLearnMoreClick = { openLearnMorePage(context) }, onCreateAccountContinue = callback::navigateToCreateAccount, onBackClick = callback::onDone, + onDeveloperSettingsClick = callback::navigateToDeveloperSettings, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 741f65234e..306549d11b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -26,10 +26,10 @@ import io.element.android.features.enterprise.api.canConnectToAnyHomeserver import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.ui.utils.MultipleTapToUnlock import kotlinx.coroutines.launch @@ -45,7 +45,6 @@ class OnBoardingPresenter( private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, private val sessionStore: SessionStore, private val accountProviderDataSource: AccountProviderDataSource, - private val loginWithClassicPresenter: Presenter, ) : Presenter { @AssistedFactory interface Factory { @@ -101,8 +100,6 @@ class OnBoardingPresenter( val loginMode by loginHelper.collectLoginMode() - val loginWithClassicState = loginWithClassicPresenter.present() - fun handleEvent(event: OnBoardingEvents) { when (event) { is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { @@ -111,6 +108,7 @@ class OnBoardingPresenter( loginHelper.submit( isAccountCreation = false, homeserverUrl = event.defaultAccountProvider, + resolvedHomeserverUrl = null, loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, ) } @@ -127,6 +125,8 @@ class OnBoardingPresenter( return OnBoardingState( isAddingAccount = isAddingAccount, + showBackButton = params.showBackButton, + showDeveloperSettings = buildMeta.buildType != BuildType.RELEASE, productionApplicationName = buildMeta.productionApplicationName, defaultAccountProvider = defaultAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider, @@ -136,7 +136,6 @@ class OnBoardingPresenter( loginMode = loginMode, version = buildMeta.versionName, onBoardingLogoResId = onBoardingLogoResId, - loginWithClassicState = loginWithClassicState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 703120b260..316efb03ef 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -10,11 +10,12 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import io.element.android.features.login.impl.login.LoginMode -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( val isAddingAccount: Boolean, + val showBackButton: Boolean, + val showDeveloperSettings: Boolean, val productionApplicationName: String, val defaultAccountProvider: String?, val mustChooseAccountProvider: Boolean, @@ -25,7 +26,6 @@ data class OnBoardingState( @DrawableRes val onBoardingLogoResId: Int?, val loginMode: AsyncData, - val loginWithClassicState: LoginWithClassicState, val eventSink: (OnBoardingEvents) -> Unit, ) { val submitEnabled: Boolean diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index 76f8eb3513..249a904dc3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -11,8 +11,6 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.login.LoginMode -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState -import io.element.android.features.login.impl.screens.onboarding.classic.aLoginWithClassicState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.R @@ -31,11 +29,17 @@ open class OnBoardingStateProvider : PreviewParameterProvider { canLoginWithQrCode = true, canCreateAccount = true, ), + anOnBoardingState( + showBackButton = true, + showDeveloperSettings = true, + ), ) } fun anOnBoardingState( isAddingAccount: Boolean = false, + showBackButton: Boolean = false, + showDeveloperSettings: Boolean = false, productionApplicationName: String = "Element", defaultAccountProvider: String? = null, mustChooseAccountProvider: Boolean = false, @@ -46,10 +50,11 @@ fun anOnBoardingState( @DrawableRes customLogoResId: Int? = null, loginMode: AsyncData = AsyncData.Uninitialized, - loginWithClassicState: LoginWithClassicState = aLoginWithClassicState(), eventSink: (OnBoardingEvents) -> Unit = {}, ) = OnBoardingState( isAddingAccount = isAddingAccount, + showBackButton = showBackButton, + showDeveloperSettings = showDeveloperSettings, productionApplicationName = productionApplicationName, defaultAccountProvider = defaultAccountProvider, mustChooseAccountProvider = mustChooseAccountProvider, @@ -59,6 +64,5 @@ fun anOnBoardingState( version = version, loginMode = loginMode, onBoardingLogoResId = customLogoResId, - loginWithClassicState = loginWithClassicState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index d590f1fec8..5ee7ab6ac4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -31,15 +31,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LifecycleEventEffect import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginModeView -import io.element.android.features.login.impl.screens.onboarding.classic.ConfirmingLoginWithElementClassic -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicEvent -import io.element.android.features.login.impl.screens.onboarding.classic.LoginWithClassicState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize @@ -47,11 +42,11 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.components.async.AsyncActionView -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton @@ -69,6 +64,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun OnBoardingView( state: OnBoardingState, onBackClick: () -> Unit, + onDeveloperSettingsClick: () -> Unit, onSignInWithQrCode: () -> Unit, onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, @@ -114,45 +110,10 @@ fun OnBoardingView( state = state, loginView = loginView, buttons = buttons, + onBackClick = onBackClick, + onDeveloperSettingsClick = onDeveloperSettingsClick, ) } - - LoginWithElementClassicView( - state = state.loginWithClassicState, - ) -} - -@Composable -private fun LoginWithElementClassicView( - state: LoginWithClassicState, -) { - LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { - state.eventSink(LoginWithClassicEvent.RefreshData) - } - AsyncActionView( - async = state.loginWithClassicAction, - confirmationDialog = { confirming -> - when (confirming) { - is ConfirmingLoginWithElementClassic -> { - // TODO i18n - ConfirmationDialog( - title = "Sign in with Element Classic", - content = "You are signing in as ${confirming.userId} on Element Classic." + - " Your existing session on Element Classic will not be signed out. Do you want to continue?", - submitText = stringResource(CommonStrings.action_continue), - onSubmitClick = { state.eventSink(LoginWithClassicEvent.DoLoginWithClassic) }, - onDismiss = { state.eventSink(LoginWithClassicEvent.CloseDialog) }, - ) - } - } - }, - onErrorDismiss = { - state.eventSink(LoginWithClassicEvent.CloseDialog) - }, - onSuccess = { - // noop, the view will be closed - } - ) } @Composable @@ -160,18 +121,49 @@ private fun AddFirstAccountScaffold( state: OnBoardingState, loginView: @Composable () -> Unit, buttons: @Composable () -> Unit, + onBackClick: () -> Unit, + onDeveloperSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { OnBoardingPage( modifier = modifier, renderBackground = state.onBoardingLogoResId == null, content = { - if (state.onBoardingLogoResId != null) { - OnBoardingLogo( - onBoardingLogoResId = state.onBoardingLogoResId, - ) - } else { - OnBoardingContent(state = state) + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.onBoardingLogoResId != null) { + OnBoardingLogo( + onBoardingLogoResId = state.onBoardingLogoResId, + ) + } else { + OnBoardingContent(state = state) + } + if (state.showDeveloperSettings) { + IconButton( + onClick = onDeveloperSettingsClick, + modifier = Modifier + .align(Alignment.TopStart), + ) { + Icon( + imageVector = CompoundIcons.SettingsSolid(), + contentDescription = stringResource(CommonStrings.common_developer_options), + ) + } + } + if (state.showBackButton) { + // Add icon button to "navigate back" + IconButton( + onClick = onBackClick, + modifier = Modifier + .align(Alignment.TopEnd), + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_cancel), + ) + } + } } loginView() }, @@ -283,18 +275,6 @@ private fun OnBoardingButtons( } else { CommonStrings.action_continue } - if (state.loginWithClassicState.canLoginWithClassic) { - Button( - text = "Sign in with Element Classic", - leadingIcon = IconSource.Vector(CompoundIcons.Mobile()), - onClick = { - state.loginWithClassicState.eventSink( - LoginWithClassicEvent.StartLoginWithClassic - ) - }, - modifier = Modifier.fillMaxWidth(), - ) - } if (state.canLoginWithQrCode) { Button( text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code), @@ -369,6 +349,7 @@ internal fun OnBoardingViewPreview( OnBoardingView( state = state, onBackClick = {}, + onDeveloperSettingsClick = {}, onSignInWithQrCode = {}, onSignIn = {}, onCreateAccount = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ElementClassicConnection.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ElementClassicConnection.kt deleted file mode 100644 index f895dd781e..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ElementClassicConnection.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.onboarding.classic - -import android.content.ComponentName -import android.content.Context -import android.content.Context.BIND_AUTO_CREATE -import android.content.Intent -import android.content.ServiceConnection -import android.os.Bundle -import android.os.Handler -import android.os.IBinder -import android.os.Message -import android.os.Messenger -import android.os.RemoteException -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import io.element.android.features.login.impl.BuildConfig -import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.di.annotations.ApplicationContext -import io.element.android.libraries.matrix.api.core.UserId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import timber.log.Timber - -interface ElementClassicConnection { - fun start() - fun stop() - fun requestData() - val stateFlow: StateFlow -} - -sealed interface ElementClassicConnectionState { - object Idle : ElementClassicConnectionState - object ElementClassicNotFound : ElementClassicConnectionState - object ElementClassicReadyNoSession : ElementClassicConnectionState - data class ElementClassicReady( - val userId: UserId, - val secrets: String, - ) : ElementClassicConnectionState - - data class Error(val error: String) : ElementClassicConnectionState -} - -private val loggerTag = LoggerTag("ECConnection") - -@ContributesBinding(AppScope::class) -class DefaultElementClassicConnection( - @ApplicationContext - private val context: Context, - @AppCoroutineScope - private val coroutineScope: CoroutineScope, -) : ElementClassicConnection { - // Messenger for communicating with the service. - private var messenger: Messenger? = null - - // Target we publish for external service to send messages to IncomingHandler. - private val incomingMessenger: Messenger = Messenger(IncomingHandler()) - - // Flag indicating whether we have called bind on the service. - private var bound: Boolean = false - - /** - * Class for interacting with the main interface of the service. - */ - private val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, service: IBinder) { - Timber.tag(loggerTag.value).d("onServiceConnected") - // This is called when the connection with the service has been - // established, giving us the object we can use to - // interact with the service. We are communicating with the - // service using a Messenger, so here we get a client-side - // representation of that from the raw IBinder object. - messenger = Messenger(service) - bound = true - // Request the data as soon as possible - requestData() - } - - override fun onServiceDisconnected(className: ComponentName) { - Timber.tag(loggerTag.value).d("onServiceDisconnected") - // This is called when the connection with the service has been - // unexpectedly disconnected—that is, its process crashed. - messenger = null - bound = false - } - } - - override fun start() { - Timber.tag(loggerTag.value).w("start()") - coroutineScope.launch { - // Establish a connection with the service. We use an explicit - // class name because there is no reason to be able to let other - // applications replace our component. - try { - val intentService = Intent() - intentService.setComponent(getElementClassicComponent()) - if (context.bindService(intentService, serviceConnection, BIND_AUTO_CREATE)) { - Timber.tag(loggerTag.value).d("Binding returned true") - } else { - // This happen when the app is not installed - Timber.tag(loggerTag.value).d("Binding returned false") - mutableStateFlow.emit(ElementClassicConnectionState.ElementClassicNotFound) - } - } catch (e: SecurityException) { - Timber.tag(loggerTag.value).e(e, "Can't bind to Service") - mutableStateFlow.emit(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) - } - } - } - - override fun stop() { - Timber.tag(loggerTag.value).w("stop(): Unbinding (bound=$bound)") - if (bound) { - // Detach our existing connection. - context.unbindService(serviceConnection) - bound = false - } - coroutineScope.launch { - mutableStateFlow.emit(ElementClassicConnectionState.Idle) - } - } - - override fun requestData() { - Timber.tag(loggerTag.value).w("requestData()") - coroutineScope.launch { - val finalMessenger = messenger - if (finalMessenger == null) { - Timber.tag(loggerTag.value).w("The messenger is null, can't request data") - mutableStateFlow.emit(ElementClassicConnectionState.Error("The messenger is null, can't request data")) - } else { - try { - // Get the data - val msg = Message.obtain(null, MSG_GET_DATA) - msg.replyTo = incomingMessenger - finalMessenger.send(msg) - } catch (e: RemoteException) { - // In this case the service has crashed before we could even - // do anything with it; we can count on soon being - // disconnected (and then reconnected if it can be restarted) - // so there is no need to do anything here. - Timber.tag(loggerTag.value).e(e, "RemoteException") - mutableStateFlow.emit(ElementClassicConnectionState.Error(e.localizedMessage.orEmpty())) - } - } - } - } - - private val mutableStateFlow = MutableStateFlow(ElementClassicConnectionState.Idle) - override val stateFlow = mutableStateFlow.asStateFlow() - - /** - * Handler of incoming messages from service. - */ - @Suppress("DEPRECATION") - inner class IncomingHandler : Handler() { - override fun handleMessage(msg: Message) { - Timber.tag(loggerTag.value).d("IncomingHandler handling message ${msg.what}") - when (msg.what) { - MSG_GET_DATA -> { - // The data must be extracted from the bundle before we launch the coroutine, else the bundle will be emptied - val state = msg.data.toElementClassicConnectionState() - emitElementClassicState(state) - } - else -> { - super.handleMessage(msg) - } - } - } - } - - private fun emitElementClassicState(state: ElementClassicConnectionState) = coroutineScope.launch { - when (state) { - is ElementClassicConnectionState.Error -> { - Timber.tag(loggerTag.value).w("Received error from Element Classic: %s", state.error) - mutableStateFlow.emit(state) - } - is ElementClassicConnectionState.ElementClassicReady -> { - Timber.tag(loggerTag.value).d("Received userId from Element Classic: %s", state.userId) - mutableStateFlow.emit(state) - } - ElementClassicConnectionState.ElementClassicReadyNoSession -> { - Timber.tag(loggerTag.value).d("Received no session from Element Classic") - mutableStateFlow.emit(state) - } - else -> { - // Should not happen - Timber.tag(loggerTag.value).w("Received unexpected state from Element Classic: %s", state) - mutableStateFlow.emit(ElementClassicConnectionState.Idle) - } - } - } - - private fun getElementClassicComponent() = ComponentName( - BuildConfig.elementClassicPackage, - ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME, - ) - - private fun Bundle?.toElementClassicConnectionState(): ElementClassicConnectionState { - return if (this == null) { - ElementClassicConnectionState.Error("No data received from Element Classic") - } else { - val error = getString(KEY_ERROR_STR) - if (error != null) { - ElementClassicConnectionState.Error(error) - } else { - val userId = getString(KEY_USER_ID_STR)?.takeIf { it.isNotEmpty() }?.let(::UserId) - if (userId != null) { - val secrets = getString(KEY_SECRETS_STR)?.takeIf { it.isNotEmpty() } - if (secrets == null) { - ElementClassicConnectionState.Error("No secrets received from Element Classic") - } else { - ElementClassicConnectionState.ElementClassicReady(userId, secrets) - } - } else { - ElementClassicConnectionState.ElementClassicReadyNoSession - } - } - } - } - - // Everything in this companion object must match what is defined in Element Classic - private companion object { - const val ELEMENT_CLASSIC_SERVICE_FULL_CLASS_NAME = "im.vector.app.features.importer.ImporterService" - - // Command to the service to get the data. - const val MSG_GET_DATA = 1 - - // Keys for the bundle returned from the service - const val KEY_ERROR_STR = "error" - const val KEY_USER_ID_STR = "userId" - - /** - * Key to extract the secrets from the bundle, as a Json string. - * Json will have this format: - * { - * "cross_signing" : { - * "master_key" : "z8RUxnaAGu___REDACTED___k+BQL9o", - * "user_signing_key" : "baJHzA___REDACTED___xMLbSUAXw9QUzqms", - * "self_signing_key" : "DU0CvLtR2G/___REDACTED___dV/MONNq4nsQhM" - * }, - * "backup" : { - * "algorithm" : "m.megolm_backup.v1.curve25519-aes-sha2", - * "key" : "VzncmQ+UOV___REDACTED___patxDz7m0Nc", - * "backup_version" : "1" - * } - * } - */ - const val KEY_SECRETS_STR = "secrets" - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicEvent.kt deleted file mode 100644 index 75a9496a02..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.onboarding.classic - -sealed interface LoginWithClassicEvent { - data object RefreshData : LoginWithClassicEvent - data object StartLoginWithClassic : LoginWithClassicEvent - data object DoLoginWithClassic : LoginWithClassicEvent - data object CloseDialog : LoginWithClassicEvent -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenter.kt deleted file mode 100644 index ef352794cb..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenter.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.onboarding.classic - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import dev.zacsweers.metro.Inject -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.toUserListFlow -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Inject -class LoginWithClassicPresenter( - private val elementClassicConnection: ElementClassicConnection, - private val sessionStore: SessionStore, - private val featureFlagService: FeatureFlagService, -) : Presenter { - @Composable - override fun present(): LoginWithClassicState { - val coroutineScope = rememberCoroutineScope() - - val isSignInWithClassicEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.SignInWithClassic) - }.collectAsState(initial = false) - - if (isSignInWithClassicEnabled) { - DisposableEffect(Unit) { - elementClassicConnection.start() - onDispose { - elementClassicConnection.stop() - } - } - } - - val state by elementClassicConnection.stateFlow.collectAsState() - val loginWithClassicAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - - val existingSession by remember { - sessionStore.sessionsFlow().toUserListFlow() - }.collectAsState(emptyList()) - - val canLoginWithClassic by remember { - derivedStateOf { - when (val finalState = state) { - is ElementClassicConnectionState.ElementClassicReady -> { - // Ensure there is no existing session with the same Id. - finalState.userId.value !in existingSession && isSignInWithClassicEnabled - } - else -> false - } - } - } - - fun handleEvent(event: LoginWithClassicEvent) { - when (event) { - LoginWithClassicEvent.RefreshData -> { - elementClassicConnection.requestData() - } - LoginWithClassicEvent.StartLoginWithClassic -> { - val currentState = elementClassicConnection.stateFlow.value - if (currentState is ElementClassicConnectionState.ElementClassicReady) { - loginWithClassicAction.value = ConfirmingLoginWithElementClassic( - userId = currentState.userId, - ) - } else { - loginWithClassicAction.value = AsyncAction.Failure(IllegalStateException("Element Classic is not ready")) - } - } - LoginWithClassicEvent.DoLoginWithClassic -> coroutineScope.launch { - // TODO Implement real login logic here - loginWithClassicAction.value = AsyncAction.Loading - delay(1000) - loginWithClassicAction.value = AsyncAction.Success(Unit) - } - LoginWithClassicEvent.CloseDialog -> { - loginWithClassicAction.value = AsyncAction.Uninitialized - } - } - } - - return LoginWithClassicState( - canLoginWithClassic = canLoginWithClassic, - loginWithClassicAction = loginWithClassicAction.value, - eventSink = ::handleEvent, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicState.kt deleted file mode 100644 index d2706fc24a..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicState.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.onboarding.classic - -import io.element.android.libraries.architecture.AsyncAction - -data class LoginWithClassicState( - val canLoginWithClassic: Boolean, - val loginWithClassicAction: AsyncAction, - val eventSink: (LoginWithClassicEvent) -> Unit, -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicStateProvider.kt deleted file mode 100644 index 73f68e5d61..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicStateProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.onboarding.classic - -import io.element.android.libraries.architecture.AsyncAction - -fun aLoginWithClassicState( - canLoginWithClassic: Boolean = false, - loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (LoginWithClassicEvent) -> Unit = {}, -) = LoginWithClassicState( - canLoginWithClassic = canLoginWithClassic, - loginWithClassicAction = loginWithClassicAction, - eventSink = eventSink, -) diff --git a/features/login/impl/src/main/res/drawable-xxhdpi/element_foss_logo.png b/features/login/impl/src/main/res/drawable-xxhdpi/element_foss_logo.png new file mode 100644 index 0000000000..67684ee944 Binary files /dev/null and b/features/login/impl/src/main/res/drawable-xxhdpi/element_foss_logo.png differ diff --git a/features/login/impl/src/main/res/drawable-xxhdpi/element_pro_logo.png b/features/login/impl/src/main/res/drawable-xxhdpi/element_pro_logo.png new file mode 100644 index 0000000000..45d11b5f2e Binary files /dev/null and b/features/login/impl/src/main/res/drawable-xxhdpi/element_pro_logo.png differ diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index 310aa22906..0de1cc0690 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -60,6 +60,8 @@ "Richiesta di accesso annullata" "L\'accesso è stato rifiutato sull\'altro dispositivo." "Accesso rifiutato" + "Non devi fare altro." + "L\'altro tuo dispositivo è già connesso" "L\'accesso è scaduto. Riprova." "L\'accesso non è stato completato in tempo" "L\'altro dispositivo non supporta l\'accesso a %s con un codice QR. diff --git a/features/login/impl/src/main/res/values-ja/translations.xml b/features/login/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..e2c89ba26e --- /dev/null +++ b/features/login/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,99 @@ + + + "アカウントの提供元を変更" + "ホームサーバーのアドレス" + "検索用のキーワードまたはドメインのアドレスを入力してください。" + "会社やコミュニティ, 個人のサーバーなどを検索します。" + "アカウントの提供元を検索" + "メールアプリのように、あなたの会話はここに保管されています。" + "%s にサインインを試みています" + "メールアプリのように、あなたの会話はここに保管されています。" + "%s にアカウントの作成を試みています" + "Matrix.org は Matrix.org Foundation が運営する、大規模で安全な分散型コミュニケーションを実現する無償のサーバーです。" + "その他" + "自身のサーバーや仕事用のアカウントにサインインするには、アカウント提供元のサーバーを指定してください。" + "アカウントの提供元を変更" + "Google Play" + "%1$s では Element Pro を使用する必要があります。アプリストアよりダウンロードしてください。" + "Element Pro が必要です" + "このホームサーバーに接続できませんでした。正しいURLを入力したことを確認し、問題が継続する場合は、ホームサーバーの管理者に問い合わせてください。" + ".well-knownファイルに問題があるためサーバーを使用できません: %1$s" + "このアカウント提供元は、スライド同期に対応していません。%1$s を使用するにはサーバーのアップグレードが必要です。" + "%2$s は %1$s からの接続を許可していません。" + "このアプリは次のサーバーを許可します: %1$s" + "アカウント提供元 %1$s は許可されていません。" + "ホームサーバーURL" + "ドメイン名を入力してください" + "サーバーのアドレスは何ですか?" + "サーバーを選択" + "アカウントを作成" + "このアカウントは無効化されています。" + "ユーザー名またはパスワードが違います" + "無効なユーザーIDです。正しい形式は \"@ユーザー:ホームサーバー\" です。" + "このサーバーはリフレッシュトークンを使用します。パスワードを使用したログインとは併用できません。" + "指定したホームサーバはパスワードまたはOIDCによるログインに対応していません。管理者に問い合わせるか、異なるホームサーバーを使用してください。" + "詳細を入力" + "Matrix は安全で分散型のオープンなネットワークです。" + "お待ちしておりました。" + "%1$s にサインイン" + "バージョン %1$s" + "手動で指定してサインイン" + "%1$s にサインイン" + "QRコードでサインイン" + "アカウントを作成" + "最速の %1$s にようこそ。機能性と利便性を極限まで追求しました。" + "機敏と利便を追求した %1$s へようこそ。" + "Be in your element" + "安全な通信を確立しています" + "新しい端末で安全な通信を確立できませんでした。既存の端末は安全な状態を維持しています。" + "どうしますか?" + "ネットワークの問題の可能性があるため、再度QRコードでログインを試してください。" + "同様の問題が発生する場合は、異なるWi-Fiやモバイルデータ通信を試してください" + "問題が解決しない場合は、手動でサインインしてください" + "接続が安全ではありません" + "この端末に表示される2つの数字の入力を要求されます" + "もう一方に表示される数字を入力してください" + "他の端末にサインインしてからもう一度試すか、既にサインインしてある端末を使用してください" + "他の端末でサインインしていません" + "もう一方の端末がサインインをキャンセルしました" + "サインインのリクエストがキャンセルされました" + "もう一方の端末でサインインを拒否されました" + "サインインを拒否" + "他には何もする必要はありません。" + "他の端末で既にサインインしています" + "サインインが無効です。もう一度試してください。" + "サインインが時間内に完了しませんでした" + "QRコードを使用した %s へのサインインに他の端末が対応していません。 + +異なる端末でQRコードを読み取るか、手動でサインインしてください。" + "QRコードに非対応" + "アカウント提供元が %1$s に対応していません。" + "%1$s に非対応" + "読み取る" + "コンピュータで %1$s を開く" + "アバターをタップしてください" + "%1$s を選択してください" + "\"新しい端末を追加\"" + "この端末でQRコードを読み取る" + "アカウント提供元が対応する場合にのみ使用できます。" + "他の端末の %1$s でQRコードを表示" + "もう一方の端末に表示されているQRコードを使用してください" + "もう一度やり直してください" + "QRコードが間違っています" + "カメラの設定を開く" + "続行するには、%1$s にカメラの使用を許可する必要があります。" + "QRコードを読み取るため、カメラへのアクセスを許可" + "QRコードを読み取り" + "やり直す" + "予期せぬ問題が発生しました。もう一度試してください。" + "一方の端末を待機しています" + "アカウント提供元が、サインインを検証するために以下の文字列を要求することがあります。" + "検証コード" + "アカウントの提供元を変更" + "Element 開発者用の非公開のサーバーです。" + "Matrix は安全で分散型のオープンなネットワークです。" + "メールアプリのように、あなたの会話はここに保管されています。" + "%1$s にサインインを試みています" + "アカウント提供元を選択" + "%1$s 上にアカウントの作成を試みています" + diff --git a/features/login/impl/src/main/res/values-lt/translations.xml b/features/login/impl/src/main/res/values-lt/translations.xml index 35e82d5a58..f8f243f29d 100644 --- a/features/login/impl/src/main/res/values-lt/translations.xml +++ b/features/login/impl/src/main/res/values-lt/translations.xml @@ -4,17 +4,29 @@ "Pagrindinio serverio adresas" "Įveskite paieškos terminą arba domeno adresą." "Ieškokite bendrovės, bendruomenės arba privataus serverio." - "Rasti paskyros teikėją" + "Raskite paskyros teikėją" "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." "Ketinate prisijungti prie %s" "Čia bus saugomi Jūsų pokalbiai - panašiai kaip el. pašto paslaugų teikėjas saugo Jūsų el. laiškus." "Ketinate sukurti paskyrą teikėjoje %s" - "Kita" - "Naudokite skirtingą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą." + "Matrix.org – tai didelis nemokamas serveris viešajame „Matrix“ tinkle saugiam ir decentralizuotam bendravimui, kurį valdo „Matrix.org“ fondas." + "Kitas" + "Naudokite kitą paskyros teikėją, pavyzdžiui, savo privatų serverį arba darbo paskyrą." "Keisti paskyros teikėją" - "Nepavyko pasiekti šio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administracija dėl tolimesnės pagalbos." - "Serverio URL" - "Koks yra Jūsų serverio adresas?" + "„Google Play“" + "„Element Pro“ programa privaloma teikėjoje %1$s. Atsisiųskite ją iš parduotuvės." + "„Element Pro“ privaloma" + "Nepavyko pasiekti šio pagrindinio serverio. Patikrinkite, ar teisingai įvedėte serverio URL. Jei URL yra teisingas, susisiekite su serverio administratoriumi dėl tolimesnės pagalbos." + "Serveris nepasiekiamas dėl problemos .labai-zinomame faile: +%1$s" + "Pasirinktas paskyros teikėjas nepalaiko slankiojo sinchronizavimo. Norint naudoti „%1$s“, reikia atnaujinti serverį." + "„%1$s“ neleidžiama prisijungti prie %2$s." + "Ši programa sukonfigūruota, kad leistų %1$s." + "Paskyros teikėjas %1$s neleidžiamas." + "Pagrindinio serverio URL" + "Įveskite domeno adresą." + "Koks yra jūsų serverio adresas?" + "Pasirinkite savo serverį" "Kurti paskyrą" "Ši paskyra buvo išjungta." "Neteisingas vartotojo vardas ir (arba) slaptažodis" @@ -31,7 +43,7 @@ "Kurti paskyrą" "Sveiki atvykę į sparčiausią „%1$s“ kada nors. Pagerintas spartai ir paprastumui." "Sveiki atvykę į „%1$s“. Pagerintas spartai ir paprastumui." - "Būkite savo elemente" + "Būkite savo stichijoje" "Keisti paskyros teikėją" "Privatus serveris “Element” darbuotojams." "Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui." diff --git a/features/login/impl/src/main/res/values-vi/translations.xml b/features/login/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b22ba5de7c --- /dev/null +++ b/features/login/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,47 @@ + + + "Thay đổi nhà cung cấp tài khoản" + "Địa chỉ Homeserver" + "Nhập từ khóa tìm kiếm hoặc địa chỉ tên miền." + "Tìm kiếm công ty, cộng đồng hoặc máy chủ riêng." + "Tìm nhà cung cấp tài khoản" + "Đây là nơi các cuộc trò chuyện của bạn sẽ được lưu — giống như bạn dùng nhà cung cấp email để giữ email của mình." + "Bạn sắp đăng nhập vào %s" + "Đây là nơi các cuộc trò chuyện của bạn sẽ được lưu — giống như bạn dùng nhà cung cấp email để giữ email của mình." + "Bạn sắp tạo tài khoản trên %s" + "Matrix.org là một máy chủ lớn, miễn phí trên mạng Matrix công cộng, cung cấp liên lạc an toàn và phi tập trung, được điều hành bởi Quỹ Matrix.org." + "Khác" + "Sử dụng nhà cung cấp tài khoản khác, ví dụ như máy chủ riêng của bạn hoặc tài khoản công việc." + "Thay đổi nhà cung cấp tài khoản" + "Chúng tớ không thể kết nối với homeserver này. Vui lòng kiểm tra xem cậu đã nhập URL homeserver chính xác chưa. Nếu URL chính xác, hãy liên hệ với quản trị viên homeserver để được hỗ trợ thêm." + "Máy chủ không khả dụng do sự cố trong tệp .well-known: +%1$s" + "URL homeserver" + "Địa chỉ máy chủ của bạn là gì?" + "Chọn máy chủ của bạn" + "Tạo tài khoản" + "Tài khoản này đã bị vô hiệu hóa." + "Tên người dùng và/hoặc mật khẩu không chính xác" + "Đây không phải là mã nhận dạng người dùng hợp lệ. Định dạng mong đợi: ‘@user:homeserver.org’" + "Máy chủ này được cấu hình sử dụng refresh token. Điều này không được hỗ trợ khi đăng nhập bằng mật khẩu." + "Homeserver đã chọn không hỗ trợ đăng nhập bằng mật khẩu hoặc OIDC. Vui lòng liên hệ với quản trị viên của cậu hoặc chọn một homeserver khác." + "Nhập thông tin chi tiết của bạn." + "Matrix là một mạng mở cho việc liên lạc an toàn và phi tập trung." + "Chào mừng bạn quay trở lại!" + "Đăng nhập vào %1$s" + "Đăng nhập thủ công" + "Đăng nhập vào %1$s" + "Đăng nhập bằng mã QR" + "Tạo tài khoản" + "Chào mừng đến với %1$s nhanh nhất từ trước đến nay. Tối ưu cho tốc độ và sự đơn giản." + "Chào mừng đến với %1$s. Tối ưu hóa cho tốc độ và sự đơn giản." + "Hãy ở trong thế mạnh (element) của mình" + "Thử lại" + "Bắt đầu lại" + "Thay đổi nhà cung cấp tài khoản" + "Máy chủ riêng dành cho nhân viên của Element." + "Matrix là một mạng mở cho việc liên lạc an toàn và phi tập trung." + "Đây là nơi các cuộc trò chuyện của bạn sẽ được lưu — giống như bạn dùng nhà cung cấp email để giữ email của mình." + "Bạn sắp đăng nhập vào %1$s" + "Bạn sắp tạo tài khoản trên %1$s" + diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 832c3b7f71..b4dee32721 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -37,11 +37,19 @@ "Matrix is an open network for secure, decentralised communication." "Welcome back!" "Sign in to %1$s" + "Open Element Classic" + "Open Element Classic on your device" + "Go to Settings > Security & Privacy" + "In Cryptography keys management, select Encrypted message recovery" + "Follow the instructions to enable your key storage" + "Come back to %1$s" + "Enable your key storage before proceeding to %1$s" "Version %1$s" "Sign in manually" "Sign in to %1$s" "Sign in with QR code" "Create account" + "Welcome back" "Welcome to the fastest %1$s ever. Supercharged for speed and simplicity." "Welcome to %1$s. Supercharged, for speed and simplicity." "Be in your element" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt index 953693b40d..86a629270f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt @@ -15,6 +15,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.FakeElementClassicConnection +import io.element.android.features.preferences.test.FakePreferencesEntryPoint import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode @@ -39,6 +41,8 @@ class DefaultLoginEntryPointTest { accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), oidcActionFlow = FakeOidcActionFlow(), appCoroutineScope = backgroundScope, + elementClassicConnection = FakeElementClassicConnection(), + preferencesEntryPoint = FakePreferencesEntryPoint(), ) } val callback = object : LoginEntryPoint.Callback { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/DefaultElementClassicConnectionTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/DefaultElementClassicConnectionTest.kt new file mode 100644 index 0000000000..5da3c97f3c --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/DefaultElementClassicConnectionTest.kt @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.impl.classic + +import android.graphics.Bitmap +import android.os.Bundle +import androidx.core.graphics.createBitmap +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.service.ServiceBinder +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.auth.ElementClassicSession +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultElementClassicConnectionTest { + @Test + fun `connection can be started Element Classic service can be bound`() = runTest { + val connection = createDefaultElementClassicConnection( + serviceBinder = FakeServiceBinder( + bindServiceResult = { + // Element Classic is found + true + }, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.start() + runCurrent() + expectNoEvents() + } + } + + @Test + fun `connection can be started Element Classic service cannot be bound`() = runTest { + val setElementClassicSessionResult = lambdaRecorder { } + val connection = createDefaultElementClassicConnection( + serviceBinder = FakeServiceBinder( + bindServiceResult = { + // Element Classic not found + false + }, + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = setElementClassicSessionResult, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.start() + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.ElementClassicNotFound) + setElementClassicSessionResult.assertions().isCalledOnce().with(value(null)) + } + } + + @Test + fun `connection cannot be started in case of security error`() = runTest { + val setElementClassicSessionResult = lambdaRecorder { } + val connection = createDefaultElementClassicConnection( + serviceBinder = FakeServiceBinder( + bindServiceResult = { throw SecurityException(A_FAILURE_REASON) }, + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = setElementClassicSessionResult, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.start() + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + setElementClassicSessionResult.assertions().isCalledOnce().with(value(null)) + } + } + + @Test + fun `requestSession when messenger is not ready has no effect`() = runTest { + val connection = createDefaultElementClassicConnection() + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.requestSession() + runCurrent() + expectNoEvents() + } + } + + @Test + fun `requestSession when the feature is disabled emits an error`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + isFeatureEnabled = false, + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + connection.requestSession() + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + } + } + + @Test + fun `when an error is received, an error is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_ERROR_STR, A_FAILURE_REASON) + } + ) + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + } + } + + @Test + fun `when there is no Element Classic session, ElementClassicReadyNoSession is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving no session from Element Classic + connection.onSessionReceived(Bundle()) + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.ElementClassicReadyNoSession) + } + } + + @Test + fun `when there is Element Classic session with empty userId, ElementClassicReadyNoSession is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving empty userId from Element Classic + connection.onSessionReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, "") + }) + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.ElementClassicReadyNoSession) + } + } + + @Test + fun `when session is received, but homeserver is not supported, an error is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(false) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isInstanceOf(ElementClassicConnectionState.Error::class.java) + } + } + + @Test + fun `when session is received without secrets, and homeserver is supported, ElementClassicReady is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with all data including key backup, and homeserver is supported, ElementClassicReady is emitted`() { + `when session is received with all data, and homeserver is supported, ElementClassicReady is emitted`( + withKeyBackup = true, + ) + } + + @Test + fun `when session is received with all data without key backup, and homeserver is supported, ElementClassicReady is emitted - backup key is missing`() { + `when session is received with all data, and homeserver is supported, ElementClassicReady is emitted`( + withKeyBackup = false, + ) + } + + private fun `when session is received with all data, and homeserver is supported, ElementClassicReady is emitted`( + withKeyBackup: Boolean, + ) = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + doSecretsContainBackupKeyResult = { _, _, _ -> withKeyBackup }, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, A_HOMESERVER_URL) + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, A_SECRET) + putString(DefaultElementClassicConnection.KEY_ROOM_KEYS_VERSION_STR, ROOM_KEYS_VERSION) + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, A_USER_NAME) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = A_HOMESERVER_URL, + secrets = A_SECRET, + roomKeysVersion = ROOM_KEYS_VERSION, + doesContainBackupKey = withKeyBackup, + ), + displayName = A_USER_NAME, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with secret but without room keys version Element Classic is outdated and the secret is ignored`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, A_HOMESERVER_URL) + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, A_SECRET) + putString(DefaultElementClassicConnection.KEY_ROOM_KEYS_VERSION_STR, null) + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, A_USER_NAME) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = A_HOMESERVER_URL, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with secret but with empty room keys version, doesContainBackupKey is false`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, A_HOMESERVER_URL) + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, A_SECRET) + putString(DefaultElementClassicConnection.KEY_ROOM_KEYS_VERSION_STR, "") + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, A_USER_NAME) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = A_HOMESERVER_URL, + secrets = A_SECRET, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + avatar = null, + ) + ) + } + } + + @Test + fun `when session is received with empty data, and homeserver is supported, ElementClassicReady is emitted`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putString(DefaultElementClassicConnection.KEY_HOMESERVER_URL_STR, "") + putString(DefaultElementClassicConnection.KEY_SECRETS_STR, "") + putString(DefaultElementClassicConnection.KEY_USER_DISPLAY_NAME_STR, "") + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + } + } + + @Test + fun `when avatar is received when the state is not ElementClassicReady, nothing happen`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving an avatar from Element Classic + connection.onAvatarReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putParcelable(DefaultElementClassicConnection.KEY_USER_AVATAR_PARCELABLE, createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + }) + runCurrent() + expectNoEvents() + } + } + + @Test + fun `when avatar is received when the state is ElementClassicReady with a different user, nothing happen`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + // Simulate receiving an avatar for another user from Element Classic + connection.onAvatarReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID_2.value) + putParcelable(DefaultElementClassicConnection.KEY_USER_AVATAR_PARCELABLE, createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + }) + runCurrent() + expectNoEvents() + } + } + + @Test + fun `when avatar is received, the state is updated`() = runTest { + val connection = createDefaultElementClassicConnection( + homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + matrixAuthenticationService = FakeMatrixAuthenticationService( + setElementClassicSessionResult = {}, + ), + ) + connection.stateFlow.test { + assertThat(awaitItem()).isEqualTo(ElementClassicConnectionState.Idle) + // Simulate receiving a session from Element Classic + connection.onSessionReceived( + Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + } + ) + assertThat(awaitItem()).isEqualTo( + ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = ElementClassicSession( + userId = A_USER_ID, + homeserverUrl = null, + secrets = null, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = null, + avatar = null, + ) + ) + // Simulate receiving an avatar from Element Classic + connection.onAvatarReceived(Bundle().apply { + putString(DefaultElementClassicConnection.KEY_USER_ID_STR, A_USER_ID.value) + putParcelable(DefaultElementClassicConnection.KEY_USER_AVATAR_PARCELABLE, createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + }) + assertThat((awaitItem() as? ElementClassicConnectionState.ElementClassicReady)?.avatar).isNotNull() + } + } + + private fun TestScope.createDefaultElementClassicConnection( + serviceBinder: ServiceBinder = FakeServiceBinder( + bindServiceResult = { true }, + unbindServiceResult = { }, + ), + coroutineScope: CoroutineScope = backgroundScope, + matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), + homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker( + checkResult = { Result.success(true) } + ), + isFeatureEnabled: Boolean = true, + featureFlagService: FeatureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SignInWithClassic.key to isFeatureEnabled, + ) + ), + ) = DefaultElementClassicConnection( + serviceBinder = serviceBinder, + coroutineScope = coroutineScope, + matrixAuthenticationService = matrixAuthenticationService, + homeServerLoginCompatibilityChecker = homeServerLoginCompatibilityChecker, + featureFlagService = featureFlagService, + ) +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/FakeElementClassicConnection.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeElementClassicConnection.kt similarity index 84% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/FakeElementClassicConnection.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeElementClassicConnection.kt index 2c41d2ed0f..227aa514b3 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/FakeElementClassicConnection.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeElementClassicConnection.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.screens.onboarding.classic +package io.element.android.features.login.impl.classic import io.element.android.tests.testutils.lambda.lambdaError import kotlinx.coroutines.flow.MutableStateFlow @@ -15,12 +15,12 @@ import kotlinx.coroutines.flow.asStateFlow class FakeElementClassicConnection( private val startResult: () -> Unit = { lambdaError() }, private val stopResult: () -> Unit = { lambdaError() }, - private val requestDataResult: () -> Unit = { lambdaError() }, + private val requestSessionResult: () -> Unit = { lambdaError() }, initialState: ElementClassicConnectionState = ElementClassicConnectionState.Idle ) : ElementClassicConnection { override fun start() = startResult() override fun stop() = stopResult() - override fun requestData() = requestDataResult() + override fun requestSession() = requestSessionResult() private val mutableStateFlow = MutableStateFlow(initialState) override val stateFlow: StateFlow = mutableStateFlow.asStateFlow() suspend fun emitState(state: ElementClassicConnectionState) { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeServiceBinder.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeServiceBinder.kt new file mode 100644 index 0000000000..0a24f13fa7 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/FakeServiceBinder.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.classic + +import android.content.Intent +import android.content.ServiceConnection +import io.element.android.libraries.androidutils.service.ServiceBinder +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeServiceBinder( + private val bindServiceResult: () -> Boolean = { lambdaError() }, + private val unbindServiceResult: () -> Unit = { lambdaError() }, +) : ServiceBinder { + override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean { + return bindServiceResult() + } + + override fun unbindService(conn: ServiceConnection) { + unbindServiceResult() + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/Fixtures.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/Fixtures.kt new file mode 100644 index 0000000000..9039743fd5 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/classic/Fixtures.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.classic + +import android.graphics.Bitmap +import io.element.android.libraries.matrix.api.auth.ElementClassicSession +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_USER_ID + +internal const val ROOM_KEYS_VERSION = "roomKeysVersion as Json data" + +fun anElementClassicReady( + elementClassicSession: ElementClassicSession = anElementClassicSession(), + displayName: String? = null, + avatar: Bitmap? = null, +) = ElementClassicConnectionState.ElementClassicReady( + elementClassicSession = elementClassicSession, + displayName = displayName, + avatar = avatar, +) + +fun anElementClassicSession( + userId: UserId = A_USER_ID, + homeserverUrl: String? = null, + secrets: String? = null, + roomKeysVersion: String? = null, + doesContainBackupKey: Boolean = false, +) = ElementClassicSession( + userId = userId, + homeserverUrl = homeserverUrl, + secrets = secrets, + roomKeysVersion = roomKeysVersion, + doesContainBackupKey = doesContainBackupKey, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelperTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelperTest.kt new file mode 100644 index 0000000000..017fd1b633 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/ClassicFlowNodeHelperTest.kt @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.login.impl.screens.classic + +import androidx.core.graphics.createBitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.features.login.impl.classic.FakeElementClassicConnection +import io.element.android.features.login.impl.classic.anElementClassicReady +import io.element.android.features.login.impl.classic.anElementClassicSession +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore +import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +// Use AndroidJUnit4 for the test with the Bitmap. +@RunWith(AndroidJUnit4::class) +class ClassicFlowNodeHelperTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `after a few seconds in Idle, NavigateToOnBoarding is emitted`() = runTest { + createHelper() + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if a session with the same account already exists`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionId = A_USER_ID.value, + ) + ) + ), + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if Element Classic is not found`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + ElementClassicConnectionState.ElementClassicNotFound + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if Element Classic has no session`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + ElementClassicConnectionState.ElementClassicReadyNoSession + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to onboarding if there has been an error`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + ElementClassicConnectionState.Error(A_FAILURE_REASON) + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToOnBoarding) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic when the session can be retrieved`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic when the session can be retrieved - ignore avatar update`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + // When the avatar is retrieved, no new event is emitted + elementClassicConnection.emitState( + anElementClassicReady( + avatar = createBitmap(1, 1) + ) + ) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic when the session can be retrieved and navigate again once the session is verified`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + secrets = A_SECRET, + ) + ) + ) + val readyState = awaitItem() + assertThat(readyState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + // When the secret with the key backup is retrieved, NavigateToLoginWithClassic is emitted again + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + secrets = A_SECRET + A_SECRET, + ) + ) + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic if a session with another account already exists`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + createHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData( + sessionId = A_USER_ID_2.value, + ) + ) + ), + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val finalState = awaitItem() + assertThat(finalState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + advanceTimeBy(10_000) + expectNoEvents() + } + } + + @Test + fun `navigate to login with classic but do not navigate to OnBoarding once the user is logged in`() = runTest { + val elementClassicConnection = FakeElementClassicConnection() + val sessionStore = InMemorySessionStore( + initialList = listOf() + ) + createHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = sessionStore, + ) + .navigationEventFlow() + .test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(NavigationEvent.Idle) + elementClassicConnection.emitState( + anElementClassicReady() + ) + val navigateToLoginWithClassicState = awaitItem() + assertThat(navigateToLoginWithClassicState).isEqualTo(NavigationEvent.NavigateToLoginWithClassic(A_USER_ID)) + // User actually logs in + sessionStore.addSession( + aSessionData( + sessionId = A_USER_ID.value, + ) + ) + advanceTimeBy(10_000) + expectNoEvents() + } + } +} + +private fun createHelper( + elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), + sessionStore: SessionStore = InMemorySessionStore(), +) = ClassicFlowNodeHelper( + elementClassicConnection = elementClassicConnection, + sessionStore = sessionStore, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/FakeLoginWithClassicNavigator.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/FakeLoginWithClassicNavigator.kt new file mode 100644 index 0000000000..e5ff91aa91 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/FakeLoginWithClassicNavigator.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeLoginWithClassicNavigator( + private val navigateToMissingKeyBackupResult: () -> Unit = { lambdaError() }, +) : LoginWithClassicNavigator { + override fun navigateToMissingKeyBackup() { + navigateToMissingKeyBackupResult() + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt new file mode 100644 index 0000000000..6b2a4fb0e1 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.loginwithclassic + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.classic.ElementClassicConnection +import io.element.android.features.login.impl.classic.ElementClassicConnectionState +import io.element.android.features.login.impl.classic.FakeElementClassicConnection +import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION +import io.element.android.features.login.impl.classic.anElementClassicReady +import io.element.android.features.login.impl.classic.anElementClassicSession +import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoginWithClassicPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.isElementPro).isFalse() + assertThat(initialState.userId).isEqualTo(A_USER_ID) + assertThat(initialState.displayName).isNull() + assertThat(initialState.avatar).isNull() + assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() + assertThat(initialState.loginMode.isUninitialized()).isTrue() + } + } + + @Test + fun `present - initial state - element Pro`() = runTest { + val presenter = createPresenter( + isEnterpriseBuild = true, + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.isElementPro).isTrue() + } + } + + @Test + fun `present - start login with correct state - user can login`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = A_SECRET, + roomKeysVersion = ROOM_KEYS_VERSION, + doesContainBackupKey = true, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + val loadingState = awaitItem() + assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue() + skipItems(1) + } + } + + @Test + fun `present - start login with no secrets - user can login and will have to verify manually`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = null, + roomKeysVersion = null, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + val loadingState = awaitItem() + assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue() + skipItems(1) + } + } + + @Test + fun `present - start login with secrets and without key backup - user will see the screen to enable key backup`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val navigateToMissingKeyBackupResult = lambdaRecorder { } + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + navigator = FakeLoginWithClassicNavigator( + navigateToMissingKeyBackupResult = navigateToMissingKeyBackupResult, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = A_SECRET, + roomKeysVersion = null, + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + navigateToMissingKeyBackupResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - start login with secrets and with invalid key backup - user will see the screen to enable key backup`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.failure(AN_EXCEPTION) + }, + ) + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val navigateToMissingKeyBackupResult = lambdaRecorder { } + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + loginHelper = createLoginHelper( + authenticationService = authenticationService, + ), + navigator = FakeLoginWithClassicNavigator( + navigateToMissingKeyBackupResult = navigateToMissingKeyBackupResult, + ), + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + anElementClassicReady( + elementClassicSession = anElementClassicSession( + userId = A_USER_ID, + secrets = A_SECRET, + roomKeysVersion = ROOM_KEYS_VERSION, + // false here + doesContainBackupKey = false, + ), + displayName = A_USER_NAME, + ) + ) + val readyState = awaitItem() + assertThat(readyState.userId).isEqualTo(A_USER_ID) + assertThat(readyState.displayName).isEqualTo(A_USER_NAME) + readyState.eventSink(LoginWithClassicEvent.Submit) + navigateToMissingKeyBackupResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - submit in wrong state and clear error`() = runTest { + val elementClassicConnection = FakeElementClassicConnection( + startResult = {}, + ) + val presenter = createPresenter( + elementClassicConnection = elementClassicConnection, + ) + presenter.test { + skipItems(1) + elementClassicConnection.emitState( + ElementClassicConnectionState.Error( + error = A_FAILURE_REASON, + ) + ) + val initialState = awaitItem() + assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() + initialState.eventSink(LoginWithClassicEvent.Submit) + val errorState = awaitItem() + assertThat(errorState.loginWithClassicAction.isFailure()).isTrue() + errorState.eventSink(LoginWithClassicEvent.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginWithClassicAction.isUninitialized()).isTrue() + } + } +} + +private fun createPresenter( + userId: UserId = A_USER_ID, + navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), + loginHelper: LoginHelper = createLoginHelper(), + elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), + accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + isEnterpriseBuild: Boolean = false, +) = LoginWithClassicPresenter( + userId = userId, + navigator = navigator, + loginHelper = loginHelper, + elementClassicConnection = elementClassicConnection, + accountProviderDataSource = accountProviderDataSource, + buildMeta = aBuildMeta( + isEnterpriseBuild = isEnterpriseBuild, + ), +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenterTest.kt new file mode 100644 index 0000000000..447b0ba77b --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupPresenterTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.classic.missingkeybackup + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.test.AN_APPLICATION_NAME +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MissingKeyBackupPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.appName).isEqualTo(AN_APPLICATION_NAME) + } + } +} + +private fun createPresenter( + buildMeta: BuildMeta = aBuildMeta(applicationName = AN_APPLICATION_NAME), +) = MissingKeyBackupPresenter( + buildMeta = buildMeta, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index 92099180ec..31a835cb8c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.A_USER_NAME_2 import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails import io.element.android.tests.testutils.WarmUpRule @@ -41,6 +42,20 @@ class LoginPasswordPresenterTest { } } + @Test + fun `present - initial login is in the first state and can be modified`() = runTest { + createLoginPasswordPresenter( + initialLogin = A_USER_NAME, + ).test { + val initialState = awaitItem() + assertThat(initialState.formState.login).isEqualTo(A_USER_NAME) + // Login can be changed + initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME_2)) + val loginChangedState = awaitItem() + assertThat(loginChangedState.formState.login).isEqualTo(A_USER_NAME_2) + } + } + @Test fun `present - enter login and password`() = runTest { val authenticationService = FakeMatrixAuthenticationService( @@ -140,9 +155,11 @@ class LoginPasswordPresenterTest { } private fun createLoginPasswordPresenter( + initialLogin: String = "", authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), ): LoginPasswordPresenter = LoginPasswordPresenter( + initialLogin = initialLogin, authenticationService = authenticationService, accountProviderDataSource = accountProviderDataSource, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 1e971ef265..1fdfb7e070 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -16,7 +16,6 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.classic.aLoginWithClassicState import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever @@ -83,16 +82,31 @@ class OnBoardingPresenterTest { ) presenter.test { val initialState = awaitItem() + assertThat(initialState.showBackButton).isFalse() assertThat(initialState.defaultAccountProvider).isNull() assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) assertThat(initialState.canReportBug).isFalse() assertThat(initialState.isAddingAccount).isFalse() - assertThat(initialState.loginWithClassicState.canLoginWithClassic).isFalse() val finalState = awaitItem() assertThat(finalState.canLoginWithQrCode).isTrue() - assertThat(finalState.loginWithClassicState.canLoginWithClassic).isFalse() + } + } + + @Test + fun `present - initial state with back button`() = runTest { + val presenter = createPresenter( + params = OnBoardingNode.Params( + accountProvider = null, + loginHint = null, + showBackButton = true, + ), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.showBackButton).isTrue() + skipItems(1) } } @@ -162,6 +176,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, @@ -184,6 +199,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, @@ -206,6 +222,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = ACCOUNT_PROVIDER_FROM_LINK, loginHint = null, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG) }, @@ -233,6 +250,7 @@ class OnBoardingPresenterTest { params = OnBoardingNode.Params( accountProvider = A_HOMESERVER_URL, loginHint = A_LOGIN_HINT, + showBackButton = false, ), enterpriseService = FakeEnterpriseService( isAllowedToConnectToHomeserverResult = { true }, @@ -265,7 +283,11 @@ class OnBoardingPresenterTest { } private fun createPresenter( - params: OnBoardingNode.Params = OnBoardingNode.Params(null, null), + params: OnBoardingNode.Params = OnBoardingNode.Params( + accountProvider = null, + loginHint = null, + showBackButton = false, + ), buildMeta: BuildMeta = aBuildMeta(), enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), @@ -287,7 +309,6 @@ private fun createPresenter( onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, sessionStore = sessionStore, accountProviderDataSource = accountProviderDataSource, - loginWithClassicPresenter = { aLoginWithClassicState() }, ) fun createLoginHelper( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index c8dcd978c6..ad09445075 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -11,9 +11,11 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues +import com.google.testing.junit.testparameterinjector.TestParameter import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData @@ -31,8 +33,9 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith +import org.robolectric.RobolectricTestParameterInjector -@RunWith(AndroidJUnit4::class) +@RunWith(RobolectricTestParameterInjector::class) class OnboardingViewTest { @get:Rule val rule = createAndroidComposeRule() @@ -44,11 +47,15 @@ class OnboardingViewTest { rule.setOnboardingView( state = anOnBoardingState( canCreateAccount = true, + showDeveloperSettings = false, eventSink = eventSink, ), onCreateAccount = callback, ) rule.clickOn(R.string.screen_onboarding_sign_up) + // Developer settings should not be shown + val developerSettingsText = rule.activity.getString(CommonStrings.common_developer_options) + rule.onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() } } @@ -83,21 +90,11 @@ class OnboardingViewTest { } @Test - fun `when can login with QR code - clicking on sign in manually calls the expected callback - can search account provider`() { - `when can login with QR code - clicking on sign in manually calls the expected callback`( - mustChooseAccountProvider = false, + fun `when can login with QR code - clicking on sign in manually calls the expected callback`( + @TestParameter mustChooseAccountProvider: Boolean = namedTestValues( + "can search account provider" to false, + "cannot search account provider" to true, ) - } - - @Test - fun `when can login with QR code - clicking on sign in manually calls the expected callback - cannot search account provider`() { - `when can login with QR code - clicking on sign in manually calls the expected callback`( - mustChooseAccountProvider = true, - ) - } - - private fun `when can login with QR code - clicking on sign in manually calls the expected callback`( - mustChooseAccountProvider: Boolean, ) { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> @@ -114,21 +111,11 @@ class OnboardingViewTest { } @Test - fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - can search account provider`() { - `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( - mustChooseAccountProvider = false, + fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( + @TestParameter mustChooseAccountProvider: Boolean = namedTestValues( + "can search account provider" to false, + "cannot search account provider" to true, ) - } - - @Test - fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback - cannot search account provider`() { - `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( - mustChooseAccountProvider = true, - ) - } - - private fun `when cannot login with QR code or create account - clicking on continue calls the sign in callback`( - mustChooseAccountProvider: Boolean, ) { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> @@ -190,6 +177,22 @@ class OnboardingViewTest { } } + @Test + fun `clicking on settings calls the developer settings callback`() { + val eventSink = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setOnboardingView( + state = anOnBoardingState( + showDeveloperSettings = true, + eventSink = eventSink, + ), + onDeveloperSettingsClick = callback, + ) + val text = rule.activity.getString(CommonStrings.common_developer_options) + rule.onNodeWithContentDescription(text).performClick() + } + } + @Test fun `cannot report a problem when the feature is disabled`() { val eventSink = EventsRecorder(expectEvents = false) @@ -253,6 +256,7 @@ class OnboardingViewTest { private fun AndroidComposeTestRule.setOnboardingView( state: OnBoardingState, onBackClick: () -> Unit = EnsureNeverCalled(), + onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(), onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onCreateAccount: () -> Unit = EnsureNeverCalled(), @@ -266,6 +270,7 @@ class OnboardingViewTest { OnBoardingView( state = state, onBackClick = onBackClick, + onDeveloperSettingsClick = onDeveloperSettingsClick, onSignInWithQrCode = onSignInWithQrCode, onSignIn = onSignIn, onCreateAccount = onCreateAccount, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenterTest.kt deleted file mode 100644 index 437e65f21d..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/LoginWithClassicPresenterTest.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.login.impl.screens.onboarding.classic - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.test.A_SECRET -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.test.InMemorySessionStore -import io.element.android.libraries.sessionstorage.test.aSessionData -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.test -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class LoginWithClassicPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state - feature disabled - start is not invoked`() = runTest { - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = { - error("start should not be invoked when feature is disabled") - }, - ) - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithClassic).isFalse() - assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - } - } - - @Test - fun `present - feature enabled - start is invoked`() = runTest { - val startResult = lambdaRecorder {} - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = startResult, - ), - isFeatureEnabled = true, - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithClassic).isFalse() - assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - val finalState = awaitItem() - assertThat(finalState.canLoginWithClassic).isFalse() - } - startResult.assertions().isCalledOnce() - } - - @Test - fun `present - emit request data invokes the expected method`() = runTest { - val requestDataResult = lambdaRecorder {} - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - requestDataResult = requestDataResult, - ), - isFeatureEnabled = true, - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.canLoginWithClassic).isFalse() - assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - val nextState = awaitItem() - assertThat(nextState.canLoginWithClassic).isFalse() - nextState.eventSink(LoginWithClassicEvent.RefreshData) - } - requestDataResult.assertions().isCalledOnce() - } - - @Test - fun `present - start login with wrong state emits an error`() = runTest { - val presenter = createPresenter( - elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ), - isFeatureEnabled = true, - ) - presenter.test { - skipItems(1) - val state = awaitItem() - state.eventSink(LoginWithClassicEvent.StartLoginWithClassic) - val errorState = awaitItem() - assertThat(errorState.loginWithClassicAction.isFailure()).isTrue() - } - } - - @Test - fun `present - start login with correct state - user cancel`() = runTest { - val elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ) - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = true, - ) - presenter.test { - skipItems(2) - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - val readyState = awaitItem() - assertThat(readyState.canLoginWithClassic).isTrue() - readyState.eventSink(LoginWithClassicEvent.StartLoginWithClassic) - val confirmingState = awaitItem() - assertThat(confirmingState.loginWithClassicAction.isConfirming()).isTrue() - assertThat((confirmingState.loginWithClassicAction as ConfirmingLoginWithElementClassic).userId).isEqualTo(A_USER_ID) - confirmingState.eventSink(LoginWithClassicEvent.CloseDialog) - val finalState = awaitItem() - assertThat(finalState.loginWithClassicAction.isUninitialized()).isTrue() - } - } - - @Test - fun `present - start login with correct state - user confirms`() = runTest { - val elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ) - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = true, - ) - presenter.test { - skipItems(2) - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - val readyState = awaitItem() - assertThat(readyState.canLoginWithClassic).isTrue() - readyState.eventSink(LoginWithClassicEvent.StartLoginWithClassic) - val confirmingState = awaitItem() - assertThat(confirmingState.loginWithClassicAction.isConfirming()).isTrue() - assertThat((confirmingState.loginWithClassicAction as ConfirmingLoginWithElementClassic).userId).isEqualTo(A_USER_ID) - confirmingState.eventSink(LoginWithClassicEvent.DoLoginWithClassic) - val loadingState = awaitItem() - assertThat(loadingState.loginWithClassicAction.isLoading()).isTrue() - val finalState = awaitItem() - assertThat(finalState.loginWithClassicAction.isSuccess()).isTrue() - } - } - - @Test - fun `present - cannot sign in if a session with the same account already exists`() = runTest { - val elementClassicConnection = FakeElementClassicConnection( - startResult = {}, - ) - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = true, - sessionStore = InMemorySessionStore( - initialList = listOf( - aSessionData( - sessionId = A_USER_ID.value, - ) - ) - ), - ) - presenter.test { - skipItems(2) - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - // No new item, because canLoginWithClassic is still false - } - } - - @Test - fun `present - cannot sign in if the feature is disabled`() = runTest { - val elementClassicConnection = FakeElementClassicConnection() - val presenter = createPresenter( - elementClassicConnection = elementClassicConnection, - isFeatureEnabled = false, - ) - presenter.test { - skipItems(1) - // Note: it should not happen IRL - elementClassicConnection.emitState( - ElementClassicConnectionState.ElementClassicReady(userId = A_USER_ID, secrets = A_SECRET) - ) - // No new item, because canLoginWithClassic is still false - } - } -} - -private fun createPresenter( - elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), - sessionStore: SessionStore = InMemorySessionStore(), - isFeatureEnabled: Boolean = false, - featureFlagService: FeatureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SignInWithClassic.key to isFeatureEnabled) - ), -) = LoginWithClassicPresenter( - elementClassicConnection = elementClassicConnection, - sessionStore = sessionStore, - featureFlagService = featureFlagService, -) diff --git a/features/logout/impl/src/main/res/values-cs/translations.xml b/features/logout/impl/src/main/res/values-cs/translations.xml index e2c2a68fd5..e19512c017 100644 --- a/features/logout/impl/src/main/res/values-cs/translations.xml +++ b/features/logout/impl/src/main/res/values-cs/translations.xml @@ -1,18 +1,18 @@ - "Opravdu se chcete odhlásit?" - "Odhlásit se" - "Odhlásit se" - "Odhlašování…" - "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám." - "Vypnuli jste zálohování" - "Když jste přešli do režimu offline, vaše klíče se ještě stále zálohovaly. Znovu se připojte, aby bylo možné před odhlášením zálohovat vaše klíče." + "Opravdu chcete odstranit toto zařízení?" + "Odebrat toto zařízení" + "Odebrat toto zařízení" + "Odebírání zařízení…" + "Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty." + "Chystáte se ztratit přístup ke svým šifrovaným chatům" + "Vaše klíče se stále zálohovaly, když jste byli offline. Před odpojením tohoto zařízení se znovu připojte, aby se vaše klíče mohly zálohovat." "Vaše klíče jsou stále zálohovány" - "Před odhlášením prosím počkejte na dokončení." + "Před odstraněním tohoto zařízení počkejte, až se proces dokončí." "Vaše klíče jsou stále zálohovány" - "Odhlásit se" - "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, ztratíte přístup ke svým šifrovaným zprávám." - "Obnovení není nastaveno" - "Chystáte se odhlásit z poslední relace. Pokud se nyní odhlásíte, můžete ztratit přístup k šifrovaným zprávám." - "Uložili jste si klíč pro obnovení?" + "Odebrat toto zařízení" + "Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty." + "Chystáte se ztratit přístup ke svým šifrovaným chatům" + "Toto je vaše jediné zařízení. Pokud ho odstraníte, budete potřebovat klíč pro obnovení, abyste si při příštím přihlášení ověřili svou digitální identitu a obnovili šifrované chaty." + "Před odebráním tohoto zařízení se ujistěte, že máte přístup ke klíči pro obnovení" diff --git a/features/logout/impl/src/main/res/values-da/translations.xml b/features/logout/impl/src/main/res/values-da/translations.xml index a828cd48b8..6bf98f8db5 100644 --- a/features/logout/impl/src/main/res/values-da/translations.xml +++ b/features/logout/impl/src/main/res/values-da/translations.xml @@ -1,17 +1,18 @@ - "Er du sikker på, at du vil logge ud?" - "Log ud" - "Log ud" - "Logger ud…" - "Du er ved at logge ud af din sidste session. Hvis du logger ud nu, mister du adgangen til dine krypterede meddelelser." - "Du har slået sikkerhedskopiering fra" - "Dine nøgler blev stadig sikkerhedskopieret, da du gik offline. Opret forbindelse igen, så dine nøgler kan sikkerhedskopieres, før du logger ud." + "Er du sikker på, at ønsker at fjerne denne enhed?" + "Fjern denne enhed" + "Fjern denne enhed" + "Fjerner enhed…" + "Dette er din eneste enhed. Hvis du fjerner den, skal du bruge en gendannelsesnøgle for at bekræfte din digitale identitet og gendanne dine krypterede chats, næste gang du logger ind." + "Du er ved at miste adgangen til dine krypterede chats" + "Dine nøgler blev stadig sikkerhedskopieret, da du gik offline. Opret forbindelse igen, så dine nøgler kan sikkerhedskopieres, før du fjerner denne enhed." "Dine nøgler bliver stadig sikkerhedskopieret" - "Vent på, at dette er fuldført, før du logger ud." + "Vent venligst, indtil dette er færdigt, før du fjerner denne enhed." "Dine nøgler bliver stadig sikkerhedskopieret" - "Log ud" - "Du er ved at logge ud af din sidste session. Hvis du logger af nu, mister du adgangen til dine krypterede meddelelser." - "Gendannelse er ikke konfigureret" - "Du er ved at logge ud af din sidste session. Hvis du logger af nu, kan du miste adgangen til dine krypterede meddelelser." + "Fjern denne enhed" + "Dette er din eneste enhed. Hvis du fjerner den, skal du bruge en gendannelsesnøgle for at bekræfte din digitale identitet og gendanne dine krypterede chats, næste gang du logger ind." + "Du er ved at miste adgangen til dine krypterede chats" + "Dette er din eneste enhed. Hvis du fjerner den, skal du bruge en gendannelsesnøgle for at bekræfte din digitale identitet og gendanne dine krypterede chats, næste gang du logger ind." + "Sørg for, at du har adgang til din gendannelsesnøgle, før du fjerner denne enhed." diff --git a/features/logout/impl/src/main/res/values-hu/translations.xml b/features/logout/impl/src/main/res/values-hu/translations.xml index 2cf2b89e4a..e7eb8de99e 100644 --- a/features/logout/impl/src/main/res/values-hu/translations.xml +++ b/features/logout/impl/src/main/res/values-hu/translations.xml @@ -1,18 +1,18 @@ - "Biztos, hogy kijelentkezik?" - "Kijelentkezés" - "Kijelentkezés" - "Kijelentkezés…" - "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez." - "Kikapcsolta a biztonsági mentést" - "A kulcsai mentése során bontotta a kapcsolatot. Kapcsolódjon újra, hogy a kulcsai továbbra is mentésre kerüljenek mielőtt kijelentkezik." + "Biztosan eltávolítja ezt az eszközt?" + "Eszköz eltávolítása" + "Eszköz eltávolítása" + "Eszköz eltávolítása…" + "Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához." + "Hamarosan elveszíti a hozzáférését a titkosított csevegéseihez" + "A kulcsok biztonsági mentése még folyamatban volt, amikor megszűnt a hálózati kapcsolat. Csatlakozzon újra, hogy a kulcsok biztonsági mentése megtörténhessen, mielőtt eltávolítja ezt az eszközt." "A kulcsai mentése még folyamatban van" - "Kijelentkezés előtt várja meg a befejezését." + "Várja meg, amíg ez befejeződik, mielőtt eltávolítja ezt az eszközt." "A kulcsai mentése még folyamatban van" - "Kijelentkezés" - "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszti a hozzáférését a titkosított üzeneteihez." - "A helyreállítás nincs beállítva" - "Arra készül, hogy kijelentkezzen az utolsó munkamenetéből is. Ha most kijelentkezik, akkor elveszítheti a hozzáférését a titkosított üzeneteihez." - "Mentette a helyreállítási kulcsát?" + "Eszköz eltávolítása" + "Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához." + "Hamarosan elveszíti a hozzáférését a titkosított csevegéseihez" + "Ez az egyetlen eszköze. Ha eltávolítja, a következő bejelentkezéskor szüksége lesz egy helyreállítási kulcsra a digitális személyazonossága megerősítéséhez és a titkosított csevegések helyreállításához." + "Az eszköz eltávolítása előtt győződjön meg arról, hogy hozzáfér a helyreállítási kulcshoz" diff --git a/features/logout/impl/src/main/res/values-it/translations.xml b/features/logout/impl/src/main/res/values-it/translations.xml index 47d6bcf519..eea93297c0 100644 --- a/features/logout/impl/src/main/res/values-it/translations.xml +++ b/features/logout/impl/src/main/res/values-it/translations.xml @@ -1,18 +1,18 @@ - "Sei sicuro di voler uscire?" - "Disconnetti" - "Disconnetti" - "Disconnessione in corso…" - "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati." - "Hai disattivato il backup" - "Il backup delle chiavi era ancora in corso quando sei andato offline. Riconnettiti per eseguire il backup delle chiavi prima di uscire." + "Sei sicuro di voler rimuovere questo dispositivo?" + "Rimuovi questo dispositivo" + "Rimuovi questo dispositivo" + "Rimozione del dispositivo…" + "Questo è il tuo unico dispositivo. Se lo rimuovi, avrai bisogno di una chiave di recupero per confermare la tua identità digitale e ripristinare le tue conversazioni cifrate al prossimo accesso." + "Stai per perdere l\'accesso alle tue conversazioni cifrate" + "Il backup delle tue chiavi era ancora in corso quando ti sei disconnesso. Riconnettiti in modo che il backup delle tue chiavi possa essere completato prima di rimuovere questo dispositivo." "Il backup delle chiavi è ancora in corso" - "Attendi il completamento dell\'operazione prima di uscire." + "Attendi il completamento dell\'operazione prima di rimuovere questo dispositivo." "Il backup delle chiavi è ancora in corso" - "Disconnetti" - "Stai per disconnettere la tua ultima sessione. Se esci ora, perderai l\'accesso ai tuoi messaggi cifrati." - "Recupero non impostato" - "Stai per disconnettere la tua ultima sessione. Se esci ora, potresti perdere l\'accesso ai tuoi messaggi cifrati." - "Hai salvato la chiave di recupero?" + "Rimuovi questo dispositivo" + "Questo è il tuo unico dispositivo. Se lo rimuovi, avrai bisogno di una chiave di recupero per confermare la tua identità digitale e ripristinare le tue conversazioni cifrate al prossimo accesso." + "Stai per perdere l\'accesso alle tue conversazioni cifrate" + "Questo è il tuo unico dispositivo. Se lo rimuovi, avrai bisogno di una chiave di recupero per confermare la tua identità digitale e ripristinare le tue conversazioni cifrate al prossimo accesso." + "Assicurati di avere accesso alla tua chiave di recupero prima di rimuovere questo dispositivo" diff --git a/features/logout/impl/src/main/res/values-ja/translations.xml b/features/logout/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..2082bacf6e --- /dev/null +++ b/features/logout/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,18 @@ + + + "本当にこの端末を削除しますか?" + "この端末を削除" + "この端末を削除" + "削除中…" + "この端末が唯一の端末です。削除を続行すると次回のログインの際に、デジタルIDと暗号化された会話を復元するために、回復鍵を入力する必要があります。" + "暗号化された会話は見られなくなります" + "鍵のバックアップ中にオフライン状態になりました。この端末を削除する前に、オンラインに復旧してバックアップを完了させてください。" + "鍵のバックアップは継続しています" + "端末の削除の前に、処理の完了をお待ち下さい。" + "鍵のバックアップは継続しています" + "この端末を削除" + "この端末が唯一の端末です。削除を続行すると次回のログインの際に、デジタルIDと暗号化された会話を復元するために、回復鍵を入力する必要があります。" + "暗号化された会話は見られなくなります" + "この端末が唯一の端末です。削除を続行すると次回のログインの際に、デジタルIDと暗号化された会話を復元するために、回復鍵を入力する必要があります。" + "この端末を削除する前に、回復鍵が手元にあることを確認してください。" + diff --git a/features/logout/impl/src/main/res/values-ru/translations.xml b/features/logout/impl/src/main/res/values-ru/translations.xml index f7ed9216c0..d96b8f24f2 100644 --- a/features/logout/impl/src/main/res/values-ru/translations.xml +++ b/features/logout/impl/src/main/res/values-ru/translations.xml @@ -1,18 +1,18 @@ - "Вы уверены, что вы хотите выйти?" + "Вы уверены, что хотите удалить это устройство?" "Удалить это устройство" "Удалить это устройство" - "Выполняется выход…" - "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям." - "Вы отключили резервное копирование" - "Когда вы перешли в автономный режим, резервное копирование ваших ключей продолжалось. Повторно подключитесь, чтобы перед выходом из системы можно было создать резервную копию ключей." + "Удаление устройства…" + "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." + "Вы потеряете доступ к своим зашифрованным чатам" + "Когда вы отключились от сети, резервное копирование ваших ключей продолжалось. Подключитесь снова, чтобы резервная копия ваших ключей была создана, прежде чем вы отключите это устройство." "Резервное копирование ключей все еще продолжается" "Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы." "Резервное копирование ключей все еще продолжается" "Удалить это устройство" - "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям." - "Восстановление не настроено" - "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям." - "Вы сохранили свой ключ восстановления?" + "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." + "Вы потеряете доступ к своим зашифрованным чатам" + "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." + "Перед тем как отключить это устройство, убедись, что у тебя есть доступ к ключу восстановления" diff --git a/features/logout/impl/src/main/res/values-sv/translations.xml b/features/logout/impl/src/main/res/values-sv/translations.xml index fdf0e5102e..c35a0455f9 100644 --- a/features/logout/impl/src/main/res/values-sv/translations.xml +++ b/features/logout/impl/src/main/res/values-sv/translations.xml @@ -1,16 +1,16 @@ - "Är du säker på att du vill logga ut?" - "Logga ut" - "Logga ut" - "Loggar ut …" + "Är du säker på att du vill ta bort den här enheten?" + "Ta bort den här enheten" + "Ta bort den här enheten" + "Tar bort enhet …" "Du är på väg att logga ut ur din senaste session. Om du loggar ut nu kommer du att förlora åtkomsten till dina krypterade meddelanden." "Du har stängt av säkerhetskopiering" "Dina nycklar säkerhetskopierades fortfarande när du gick offline. Anslut igen så att dina nycklar kan säkerhetskopieras innan du loggar ut." "Dina nycklar säkerhetskopieras fortfarande" "Vänta tills detta är klart innan du loggar ut." "Dina nycklar säkerhetskopieras fortfarande" - "Logga ut" + "Ta bort den här enheten" "Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden." "Återställning inte inställd" "Du är på väg att logga ut från din senaste session. Om du loggar ut nu kan du förlora åtkomsten till dina krypterade meddelanden." diff --git a/features/logout/impl/src/main/res/values-vi/translations.xml b/features/logout/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..6c1a028948 --- /dev/null +++ b/features/logout/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,18 @@ + + + "Bạn có chắc muốn gỡ thiết bị này không?" + "Gỡ bỏ thiết bị này" + "Gỡ bỏ thiết bị này" + "Đang gỡ thiết bị…" + "Đây là thiết bị duy nhất của bạn. Nếu xóa nó, bạn sẽ cần khóa khôi phục để xác nhận danh tính kỹ thuật số và khôi phục các cuộc trò chuyện được mã hóa lần tiếp theo khi đăng nhập." + "Bạn sắp mất quyền truy cập vào các cuộc trò chuyện được mã hóa" + "Bạn đã ngoại tuyến khi các khóa đang được sao lưu. Kết nối lại để hoàn tất sao lưu trước khi gỡ thiết bị." + "Khóa của bạn vẫn đang được sao lưu." + "Đợi quá trình hoàn tất rồi hãy gỡ thiết bị." + "Khóa của bạn vẫn đang được sao lưu." + "Gỡ bỏ thiết bị này" + "Bạn sắp đăng xuất khỏi phiên làm việc cuối cùng. Nếu bạn đăng xuất ngay bây giờ, bạn sẽ mất quyền truy cập vào các tin nhắn đã mã hóa của mình." + "Bạn sắp mất quyền truy cập vào các cuộc trò chuyện được mã hóa" + "Đây là thiết bị duy nhất của bạn. Nếu xóa nó, bạn sẽ cần khóa khôi phục để xác nhận danh tính kỹ thuật số và khôi phục các cuộc trò chuyện được mã hóa khi đăng nhập lần tới." + "Đảm bảo bạn có khóa khôi phục trước khi gỡ thiết bị này." + diff --git a/features/logout/impl/src/main/res/values-zh/translations.xml b/features/logout/impl/src/main/res/values-zh/translations.xml index 0a8ec07e87..d438c64ff3 100644 --- a/features/logout/impl/src/main/res/values-zh/translations.xml +++ b/features/logout/impl/src/main/res/values-zh/translations.xml @@ -1,16 +1,16 @@ - "确定要登出吗?" - "登出" - "登出" - "正在登出…" + "您确定要删除此设备吗?" + "删除此设备" + "删除此设备" + "正在删除设备……" "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" "您已关闭备份" "当你离线时,密钥仍在备份中。重新连接以便在登出之前备份密钥。" "您的密钥仍在备份中" "请等待此操作完成后再登出。" "您的密钥仍在备份中" - "登出" + "删除此设备" "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" "未设置恢复" "即将登出最后一个会话。如果现在登出,将无法访问加密的消息。" diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index a23e337d2a..3eecd54f3e 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -38,6 +38,7 @@ interface MessagesEntryPoint : FeatureEntryPoint { fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) fun navigateToRoom(roomId: RoomId) + fun navigateToDeveloperSettings() } data class Params(val initialTarget: InitialTarget) : NodeInputs diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index dd2b695022..d3455fa487 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(projects.libraries.preferences.api) implementation(projects.libraries.recentemojis.api) implementation(projects.libraries.roomselect.api) + implementation(projects.libraries.slashcommands.api) implementation(projects.libraries.audio.api) implementation(projects.libraries.voiceplayer.api) implementation(projects.libraries.voicerecorder.api) @@ -105,4 +106,5 @@ dependencies { testImplementation(projects.features.poll.test) testImplementation(projects.libraries.eventformatter.test) testImplementation(projects.libraries.recentemojis.test) + testImplementation(projects.libraries.slashcommands.test) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 31545a0597..a268334ef4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimel import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.threads.ThreadedMessagesNode +import io.element.android.features.messages.impl.threads.list.ThreadsListNode import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -197,6 +198,9 @@ class MessagesFlowNode( val recipientAddress: String?, val amountLovelace: Long?, ) : NavTarget + + @Parcelize + data object ThreadsList : NavTarget } private val callback: MessagesEntryPoint.Callback = callback() @@ -324,6 +328,14 @@ class MessagesFlowNode( ) { backstack.push(NavTarget.PaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)) } + + override fun navigateToThreadsList() { + backstack.push(NavTarget.ThreadsList) + } + + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } } val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId) createNode(buildContext, listOf(callback, inputs)) @@ -459,6 +471,10 @@ class MessagesFlowNode( override fun handleForwardEventClick(eventId: EventId) { backstack.push(NavTarget.ForwardEvent(eventId = eventId, fromPinnedEvents = true)) } + + override fun navigateToThread(threadRootId: ThreadId) { + backstack.push(NavTarget.Thread(threadRootId, null)) + } } createNode(buildContext, plugins = listOf(callback)) } @@ -542,6 +558,10 @@ class MessagesFlowNode( ) { backstack.push(NavTarget.PaymentFlow(roomId, recipientUserId, recipientAddress, amountLovelace)) } + + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } } createNode(buildContext, listOf(inputs, callback)) } @@ -601,6 +621,14 @@ class MessagesFlowNode( .setAmount(navTarget.amountLovelace?.toString()) .build() } + NavTarget.ThreadsList -> { + val callback = object : ThreadsListNode.Callback { + override fun openThread(threadId: ThreadId) { + backstack.push(NavTarget.Thread(threadId, focusedEventId = null)) + } + } + createNode(buildContext, listOf(callback)) + } } } @@ -663,7 +691,7 @@ class MessagesFlowNode( assetType = event.content.assetType, ) NavTarget.LocationViewer( - mode = mode + mode = mode ).takeIf { locationService.isServiceAvailable() } } else -> null diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt index 0a317abbb0..202affad57 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -23,7 +23,9 @@ interface MessagesNavigator { fun navigateToEditPoll(eventId: EventId) fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List) + fun navigateToMember(userId: UserId) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) + fun navigateToDeveloperSettings() /** * Navigate to the payment flow for /pay slash command. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 41ccb686cd..8b338e78ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -105,7 +105,7 @@ class MessagesNode( private val timelineController = TimelineController(room, room.liveTimeline) private val presenter = presenterFactory.create( navigator = this, - composerPresenter = messageComposerPresenterFactory.create(timelineController, this), + composerPresenter = messageComposerPresenterFactory.create(timelineController, this, isInThread = false), timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), actionListPresenter = actionListPresenterFactory.create( postProcessor = TimelineItemActionPostProcessor.Default, @@ -130,6 +130,8 @@ class MessagesNode( fun navigateToRoomDetails() fun navigateToPinnedMessagesList() fun navigateToKnockRequestsList() + fun navigateToDeveloperSettings() + fun navigateToThreadsList() fun navigateToWallet() fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?) } @@ -224,10 +226,18 @@ class MessagesNode( } } + override fun navigateToMember(userId: UserId) { + callback.navigateToRoomMemberDetails(userId) + } + override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { callback.navigateToThread(threadRootId, focusedEventId) } + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } + override fun navigateToPaymentFlow( roomId: RoomId, recipientUserId: UserId?, @@ -302,6 +312,7 @@ class MessagesNode( onViewRequestsClick = callback::navigateToKnockRequestsList, ) }, + onThreadsListClick = callback::navigateToThreadsList, ) roomMemberModerationRenderer.Render( state = state.roomMemberModerationState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index c71144529d..e5ef451733 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -27,6 +28,7 @@ import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.appconfig.MessageComposerConfig import io.element.android.features.messages.api.timeline.HtmlConverterProvider +import io.element.android.features.messages.impl.MessagesState.Threads import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState @@ -85,8 +87,11 @@ import io.element.android.libraries.recentemojis.api.AddRecentEmoji import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -160,6 +165,13 @@ class MessagesPresenter( val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() val roomCallState = roomCallStatePresenter.present() val roomMemberModerationState = roomMemberModerationPresenter.present() + val threadsList by produceState(persistentListOf()) { + room.threadsListService.subscribeToItemUpdates() + .onStart { room.threadsListService.paginate() } + .collectLatest { value = it.toImmutableList() } + } + + val canOpenThreadList by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomThreadList).collectAsState(initial = false) val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms -> perms.userEventPermissions() @@ -250,12 +262,11 @@ class MessagesPresenter( is MessagesEvent.OnUserClicked -> { roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) } - is MessagesEvent.MarkAsFullyReadAndExit -> coroutineScope.launch { - if (!markingAsReadAndExiting.getAndSet(true)) { + is MessagesEvent.MarkAsFullyReadAndExit -> if (!markingAsReadAndExiting.getAndSet(true)) { + coroutineScope.launch { val latestEventId = room.liveTimeline.getLatestEventId().getOrElse { Timber.w(it, "Failed to get latest event id to mark as fully read") - navigator.close() - return@launch + null } latestEventId?.let { eventId -> sessionCoroutineScope.launch { @@ -263,7 +274,6 @@ class MessagesPresenter( } } navigator.close() - markingAsReadAndExiting.set(false) } } } @@ -297,6 +307,11 @@ class MessagesPresenter( topBarSharedHistoryIcon = topBarSharedHistoryIcon, isDmRoom = roomInfo.isDm, successorRoom = roomInfo.successorRoom, + threads = Threads( + hasThreads = canOpenThreadList && threadsList.isNotEmpty(), + // TODO calculate this properly based on the thread list and the read state of each thread + hasUnreadThreads = false, + ), eventSink = ::handleEvent, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 1b67cb6929..7d004bfa5b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -58,9 +58,15 @@ data class MessagesState( val topBarSharedHistoryIcon: SharedHistoryIcon, val isDmRoom: Boolean, val successorRoom: SuccessorRoom?, + val threads: Threads, val eventSink: (MessagesEvent) -> Unit ) { val isTombstoned = successorRoom != null + + data class Threads( + val hasThreads: Boolean, + val hasUnreadThreads: Boolean, + ) } /** Type of "shared history" icon to show in the top bar. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index e9555f656d..b11ca17d73 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -123,6 +123,10 @@ fun aMessagesState( topBarSharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, isDmRoom: Boolean = false, successorRoom: SuccessorRoom? = null, + threads: MessagesState.Threads = MessagesState.Threads( + hasThreads = false, + hasUnreadThreads = false, + ), eventSink: (MessagesEvent) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -152,6 +156,7 @@ fun aMessagesState( topBarSharedHistoryIcon = topBarSharedHistoryIcon, isDmRoom = isDmRoom, successorRoom = successorRoom, + threads = threads, eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 2bb320fdba..ba90be34b1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -12,10 +12,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -26,6 +28,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -39,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -51,6 +55,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.ActionListView @@ -73,6 +78,7 @@ import io.element.android.features.messages.impl.timeline.aGroupedEvents import io.element.android.features.messages.impl.timeline.aTimelineItemDaySeparator import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent @@ -87,6 +93,7 @@ import io.element.android.features.messages.impl.topbars.MessagesViewTopBar import io.element.android.features.messages.impl.topbars.ThreadTopBar import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog +import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout @@ -98,6 +105,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed @@ -133,6 +142,7 @@ fun MessagesView( onJoinCallClick: (isAudioCall: Boolean) -> Unit, onWalletClick: () -> Unit, onViewAllPinnedMessagesClick: () -> Unit, + onThreadsListClick: () -> Unit, modifier: Modifier = Modifier, forceJumpToBottomVisibility: Boolean = false, knockRequestsBannerView: @Composable () -> Unit, @@ -224,14 +234,20 @@ fun MessagesView( roomAvatar = state.roomAvatar, isTombstoned = state.isTombstoned, heroes = state.heroes, - roomCallState = state.roomCallState, dmUserIdentityState = state.dmUserVerificationState, sharedHistoryIcon = state.topBarSharedHistoryIcon, - isDmRoom = state.isDmRoom, onBackClick = { hidingKeyboard { onBackClick() } }, onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, - onJoinCallClick = onJoinCallClick, - onWalletClick = onWalletClick, + menuActions = { + MessagesMenuActions( + displayThreads = state.timelineState.timelineMode !is Timeline.Mode.Thread && state.threads.hasThreads, + roomCallState = state.roomCallState, + onJoinCallClick = onJoinCallClick, + onThreadsListClick = onThreadsListClick, + isDmRoom = state.isDmRoom, + onWalletClick = onWalletClick, + ) + } ) } }, @@ -400,6 +416,40 @@ fun MessagesView( ) } +@Composable +internal fun MessagesMenuActions( + displayThreads: Boolean, + roomCallState: RoomCallState, + onJoinCallClick: (isAudioCall: Boolean) -> Unit, + onThreadsListClick: () -> Unit, + isDmRoom: Boolean = false, + onWalletClick: (() -> Unit)? = null, +) { + if (displayThreads) { + Icon( + modifier = Modifier.clickable(enabled = true, onClick = onThreadsListClick), + imageVector = CompoundIcons.ThreadsSolid(), + contentDescription = stringResource(CommonStrings.common_threads), + ) + Spacer(Modifier.width(8.dp)) + } + CallMenuItem( + roomCallState = roomCallState, + onJoinCallClick = onJoinCallClick, + ) + // Wallet button - only show in DM rooms + if (isDmRoom && onWalletClick != null) { + Spacer(Modifier.width(8.dp)) + IconButton(onClick = onWalletClick) { + Icon( + imageVector = CompoundIcons.Chart(), + contentDescription = "Cardano Wallet", + ) + } + } + Spacer(Modifier.width(8.dp)) +} + @Composable private fun ReinviteDialog(state: MessagesState) { if (state.showReinvitePrompt) { @@ -469,6 +519,9 @@ private fun MessagesViewContent( val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior( pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0, ) + val density = LocalDensity.current + var pinnedBannerHeightDp by remember { mutableStateOf(0.dp) } + TimelineView( state = state.timelineState, timelineProtectionState = state.timelineProtectionState, @@ -484,11 +537,13 @@ private fun MessagesViewContent( forceJumpToBottomVisibility = forceJumpToBottomVisibility, onJoinCallClick = onJoinCallClick, nestedScrollConnection = scrollBehavior.nestedScrollConnection, + floatingDateTopOffset = pinnedBannerHeightDp, ) if (state.timelineState.timelineMode !is Timeline.Mode.Thread) { AnimatedVisibility( visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, + modifier = Modifier.onSizeChanged { pinnedBannerHeightDp = with(density) { it.height.toDp() } }, enter = expandVertically(), exit = shrinkVertically(), ) { @@ -601,6 +656,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onViewAllPinnedMessagesClick = { }, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, + onThreadsListClick = {}, ) } @@ -653,7 +709,8 @@ internal fun MessagesViewA11yPreview() = ElementPreview { onCreatePollClick = {}, onJoinCallClick = {}, onWalletClick = {}, - onViewAllPinnedMessagesClick = { }, + onViewAllPinnedMessagesClick = {}, + onThreadsListClick = {}, forceJumpToBottomVisibility = true, knockRequestsBannerView = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index 1ca1df0393..56e060cd74 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -42,6 +42,7 @@ internal fun MessagesViewWithIdentityChangePreview( onJoinCallClick = {}, onWalletClick = {}, onViewAllPinnedMessagesClick = {}, - knockRequestsBannerView = {} + knockRequestsBannerView = {}, + onThreadsListClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvent.kt index ae82c60f2a..982ca7dfd7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvent.kt @@ -36,4 +36,5 @@ sealed interface MessageComposerEvent { data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvent data class InsertSuggestion(val resolvedSuggestion: ResolvedSuggestion) : MessageComposerEvent data object SaveDraft : MessageComposerEvent + data object ClearSlashError : MessageComposerEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 0fa3e917a5..89ab1d76a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -15,6 +15,7 @@ import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -33,14 +34,16 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.location.api.LocationService import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.Attachment.Media +import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.wallet.impl.slash.ParsedPayCommand import io.element.android.features.wallet.impl.slash.SlashCommandParser -import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.utils.TextPillificationHelper +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes @@ -70,6 +73,9 @@ import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.api.message import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState @@ -107,6 +113,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes class MessageComposerPresenter( @Assisted private val navigator: MessagesNavigator, @Assisted private val timelineController: TimelineController, + @Assisted private val isInThread: Boolean, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val room: JoinedRoom, private val mediaPickerProvider: PickerProvider, @@ -129,10 +136,15 @@ class MessageComposerPresenter( private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, private val notificationConversationService: NotificationConversationService, private val slashCommandParser: SlashCommandParser, + private val slashCommandService: SlashCommandService, ) : Presenter { @AssistedFactory interface Factory { - fun create(timelineController: TimelineController, navigator: MessagesNavigator): MessageComposerPresenter + fun create( + timelineController: TimelineController, + navigator: MessagesNavigator, + isInThread: Boolean, + ): MessageComposerPresenter } private val mediaSender = mediaSenderFactory.create(timelineMode = timelineController.mainTimelineMode()) @@ -222,6 +234,8 @@ class MessageComposerPresenter( } ) + val slashCommandAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + LaunchedEffect(Unit) { val draft = draftService.loadDraft( roomId = room.roomId, @@ -250,12 +264,13 @@ class MessageComposerPresenter( sessionCoroutineScope.sendMessage( markdownTextEditorState = markdownTextEditorState, richTextEditorState = richTextEditorState, + slashCommandAction = slashCommandAction, ) } is MessageComposerEvent.SendUri -> { val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId sessionCoroutineScope.sendAttachment( - attachment = Attachment.Media( + attachment = Media( localMedia = localMediaFactory.createFromUri( uri = event.uri, mimeType = null, @@ -345,9 +360,7 @@ class MessageComposerPresenter( richTextEditorState.insertMentionAtSuggestion(text = text, link = link) } is ResolvedSuggestion.Command -> { - // Insert the command text with a trailing space - richTextEditorState.setMarkdown("${suggestion.command} ") - suggestionSearchTrigger.value = null + richTextEditorState.replaceSuggestion(suggestion.command.command) } } } else if (markdownTextEditorState.currentSuggestion != null) { @@ -363,6 +376,9 @@ class MessageComposerPresenter( val draft = createDraftFromState(markdownTextEditorState, richTextEditorState) sessionCoroutineScope.updateDraft(draft, isVolatile = false) } + MessageComposerEvent.ClearSlashError -> { + slashCommandAction.value = AsyncAction.Uninitialized + } } } @@ -394,6 +410,7 @@ class MessageComposerPresenter( suggestions = suggestions.toImmutableList(), resolveMentionDisplay = resolveMentionDisplay, resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay, + slashCommandAction = slashCommandAction.value, eventSink = ::handleEvent, ) } @@ -431,6 +448,7 @@ class MessageComposerPresenter( roomAliasSuggestions = roomAliasSuggestions, currentUserId = currentUserId, canSendRoomMention = ::canSendRoomMention, + isInThread = isInThread, ) suggestions.clear() suggestions.addAll(result) @@ -442,53 +460,115 @@ class MessageComposerPresenter( private fun CoroutineScope.sendMessage( markdownTextEditorState: MarkdownTextEditorState, richTextEditorState: RichTextEditorState, + slashCommandAction: MutableState>, ) = launch { val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = true) val capturedMode = messageComposerContext.composerMode - // Check for /pay slash command - val payCommand = parsePayCommand(message.markdown) - if (payCommand != null) { - when (payCommand) { - is io.element.android.features.wallet.impl.slash.ParsedPayCommand.ParseError -> { - // Show error, keep text in composer - snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) - return@launch + // Check for /pay slash command FIRST (our Cardano wallet integration). + // If matched, short-circuit before upstream's slash command service runs. + if (capturedMode is MessageComposerMode.Normal) { + val payCommand = parsePayCommand(message.markdown) + if (payCommand != null) { + when (payCommand) { + is ParsedPayCommand.ParseError -> { + // Show error, keep text in composer + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) + return@launch + } + is ParsedPayCommand.WithAddressRecipient -> { + // Reset composer and navigate to payment flow + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + recipientAddress = payCommand.address, + amountLovelace = payCommand.amount, + ) + return@launch + } + is ParsedPayCommand.WithMatrixRecipient -> { + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + recipientUserId = payCommand.matrixUserId, + amountLovelace = payCommand.amount, + ) + return@launch + } + is ParsedPayCommand.AmountOnly -> { + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + amountLovelace = payCommand.amount, + ) + return@launch + } + is ParsedPayCommand.Empty -> { + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) + navigator.navigateToPaymentFlow( + roomId = room.roomId, + ) + return@launch + } } - is io.element.android.features.wallet.impl.slash.ParsedPayCommand.WithAddressRecipient -> { - // Reset composer and navigate to payment flow - resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) - navigator.navigateToPaymentFlow( - roomId = room.roomId, - recipientAddress = payCommand.address, - amountLovelace = payCommand.amount, - ) - return@launch + } + } + + val slashCommand = if (capturedMode is MessageComposerMode.Normal) { + slashCommandService.parse( + textMessage = message.markdown, + formattedMessage = message.html, + isInThreadTimeline = isInThread, + ) + } else { + SlashCommand.NotACommand + } + + when (slashCommand) { + is SlashCommand.NotACommand -> Unit + is SlashCommand.Error -> { + slashCommandAction.value = AsyncAction.Failure(Exception(slashCommand.message())) + return@launch + } + is SlashCommand.SlashCommandNavigation -> { + when (slashCommand) { + is SlashCommand.ShowUser -> { + navigator.navigateToMember(slashCommand.userId) + } + SlashCommand.DevTools -> { + navigator.navigateToDeveloperSettings() + } } - is io.element.android.features.wallet.impl.slash.ParsedPayCommand.WithMatrixRecipient -> { - resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) - navigator.navigateToPaymentFlow( - roomId = room.roomId, - recipientUserId = payCommand.matrixUserId, - amountLovelace = payCommand.amount, - ) - return@launch - } - is io.element.android.features.wallet.impl.slash.ParsedPayCommand.AmountOnly -> { - resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) - navigator.navigateToPaymentFlow( - roomId = room.roomId, - amountLovelace = payCommand.amount, - ) - return@launch - } - is io.element.android.features.wallet.impl.slash.ParsedPayCommand.Empty -> { - resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = false) - navigator.navigateToPaymentFlow( - roomId = room.roomId, - ) - return@launch + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit) + return@launch + } + is SlashCommand.SlashCommandSendMessage -> { + timelineController.invokeOnCurrentTimeline { + slashCommandService.proceedSendMessage(slashCommand, this) + .onFailure { cause -> + Timber.e(cause, "Failed to proceed with admin slash command") + slashCommandAction.value = AsyncAction.Failure(cause) + } + .onSuccess { + // Reset composer + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit) + } } + return@launch + } + is SlashCommand.SlashCommandAdmin -> { + slashCommandAction.value = AsyncAction.Loading + slashCommandService.proceedAdmin(slashCommand) + .onFailure { cause -> + Timber.e(cause, "Failed to proceed with admin slash command") + slashCommandAction.value = AsyncAction.Failure(cause) + } + .onSuccess { + // Reset composer + resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit) + slashCommandAction.value = AsyncAction.Uninitialized + } + return@launch } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 424e8c07b9..f3fdb3d59a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Stable +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.TextEditorState @@ -26,5 +27,6 @@ data class MessageComposerState( val suggestions: ImmutableList, val resolveMentionDisplay: (String, String) -> TextDisplay, val resolveAtRoomMentionDisplay: () -> TextDisplay, + val slashCommandAction: AsyncAction, val eventSink: (MessageComposerEvent) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index a06bf30dad..ef9cd7933b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.TextEditorState @@ -32,6 +33,7 @@ fun aMessageComposerState( showAttachmentSourcePicker: Boolean = false, canShareLocation: Boolean = true, suggestions: ImmutableList = persistentListOf(), + slashCommandAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (MessageComposerEvent) -> Unit = {}, ) = MessageComposerState( textEditorState = textEditorState, @@ -43,5 +45,6 @@ fun aMessageComposerState( suggestions = suggestions, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, resolveAtRoomMentionDisplay = { TextDisplay.Plain }, + slashCommandAction = slashCommandAction, eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 4b346e0c15..d387bc8765 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -22,6 +22,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer. import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState +import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.textcomposer.TextComposer @@ -115,6 +116,12 @@ internal fun MessageComposerView( onTyping = ::onTyping, onSelectRichContent = ::sendUri, ) + + AsyncActionView( + async = state.slashCommandAction, + onSuccess = {}, + onErrorDismiss = { state.eventSink(MessageComposerEvent.ClearSlashError) }, + ) } @PreviewsDayNight diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt index 4ab7c297d3..678ef2ba56 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.avatar.AvatarType.Room import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -40,6 +41,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -63,7 +65,7 @@ fun SuggestionsPickerView( is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Alias -> suggestion.roomId.value - is ResolvedSuggestion.Command -> suggestion.command + is ResolvedSuggestion.Command -> suggestion.command.command } } ) { @@ -92,58 +94,81 @@ private fun SuggestionItemView( modifier: Modifier = Modifier, ) { Row( - modifier = modifier.clickable { onSelectSuggestion(suggestion) }, - horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .clickable { onSelectSuggestion(suggestion) } + .padding(horizontal = 16.dp), ) { val avatarSize = AvatarSize.Suggestion val avatarData = when (suggestion) { is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize) is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize) is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize) - is ResolvedSuggestion.Command -> AvatarData(suggestion.command, suggestion.command, null, avatarSize) + is ResolvedSuggestion.Command -> null } val avatarType = when (suggestion) { - is ResolvedSuggestion.Alias, - is ResolvedSuggestion.Command -> AvatarType.Room() + is ResolvedSuggestion.Alias -> Room() ResolvedSuggestion.AtRoom, is ResolvedSuggestion.Member -> AvatarType.User + is ResolvedSuggestion.Command -> null } val title = when (suggestion) { is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedSuggestion.Member -> suggestion.roomMember.displayName is ResolvedSuggestion.Alias -> suggestion.roomName - is ResolvedSuggestion.Command -> suggestion.command + is ResolvedSuggestion.Command -> suggestion.command.command + } + val details = when (suggestion) { + is ResolvedSuggestion.AtRoom, + is ResolvedSuggestion.Member, + is ResolvedSuggestion.Alias -> null + is ResolvedSuggestion.Command -> suggestion.command.parameters } val subtitle = when (suggestion) { is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Alias -> suggestion.roomAlias.value - is ResolvedSuggestion.Command -> suggestion.description + is ResolvedSuggestion.Command -> suggestion.command.description + } + if (avatarData != null && avatarType != null) { + Avatar( + avatarData = avatarData, + avatarType = avatarType, + modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 16.dp), + ) } - Avatar( - avatarData = avatarData, - avatarType = avatarType, - modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp), - ) Column( modifier = Modifier .fillMaxWidth() - .padding(end = 16.dp, top = 8.dp, bottom = 8.dp) + .padding(top = 8.dp, bottom = 8.dp) .align(Alignment.CenterVertically), ) { - title?.let { - Text( - text = it, - style = ElementTheme.typography.fontBodyLgRegular, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + title?.let { + Text( + text = it, + style = ElementTheme.typography.fontBodyLgRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + details?.let { + Text( + text = it, + style = ElementTheme.typography.fontBodyMdRegular, + maxLines = 1, + color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Ellipsis, + ) + } } Text( text = subtitle, style = ElementTheme.typography.fontBodySmRegular, color = ElementTheme.colors.textSecondary, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) } @@ -179,7 +204,21 @@ internal fun SuggestionsPickerViewPreview() { roomId = RoomId("!room:matrix.org"), roomName = "My room", roomAvatarUrl = null, - ) + ), + ResolvedSuggestion.Command( + command = SlashCommandSuggestion( + command = "/noparam", + parameters = null, + description = "A slash command without parameters", + ) + ), + ResolvedSuggestion.Command( + command = SlashCommandSuggestion( + command = "/withparam", + parameters = " [reason]", + description = "A slash command with parameters", + ) + ), ), onSelectSuggestion = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt index dde6f49378..5fd4a4e067 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessor.kt @@ -15,6 +15,8 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType @@ -23,7 +25,9 @@ import io.element.android.libraries.textcomposer.model.SuggestionType * This class is responsible for processing suggestions when `@`, `/` or `#` are type in the composer. */ @Inject -class SuggestionsProcessor { +class SuggestionsProcessor( + private val slashCommandService: SlashCommandService, +) { /** * Process the suggestion. * @param suggestion The current suggestion input @@ -31,6 +35,7 @@ class SuggestionsProcessor { * @param roomAliasSuggestions The available room alias suggestions * @param currentUserId The current user id * @param canSendRoomMention Should return true if the current user can send room mentions + * @param isInThread Whether the composer is in a thread or not, used to filter slash commands suggestions * @return The list of suggestions to display */ suspend fun process( @@ -39,6 +44,7 @@ class SuggestionsProcessor { roomAliasSuggestions: List, currentUserId: UserId, canSendRoomMention: suspend () -> Boolean, + isInThread: Boolean, ): List { suggestion ?: return emptyList() return when (suggestion.type) { @@ -70,14 +76,27 @@ class SuggestionsProcessor { } } SuggestionType.Command -> { - // Return available slash commands filtered by user input - val commands = listOf( - ResolvedSuggestion.Command("/pay", "Send ADA to someone"), - ) - commands.filter { command -> - // Filter by what user has typed after / - command.command.contains(suggestion.text, ignoreCase = true) || - suggestion.text.isEmpty() + // Command suggestions are valid only if this is the beginning of the message + if (suggestion.start == 0) { + val upstream = slashCommandService.getSuggestions(suggestion.text, isInThread).map { + ResolvedSuggestion.Command(it) + } + val wallet = if ("pay".startsWith(suggestion.text, ignoreCase = true)) { + listOf( + ResolvedSuggestion.Command( + SlashCommandSuggestion( + command = "pay", + parameters = "[recipient] [amount]", + description = "Send ADA to someone", + ) + ) + ) + } else { + emptyList() + } + upstream + wallet + } else { + emptyList() } } SuggestionType.Emoji, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt index 6ad4fbefe6..73ba08b29d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt @@ -10,7 +10,9 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.matrix.api.core.ThreadId sealed interface PinnedMessagesListEvent { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : PinnedMessagesListEvent + data class OpenThread(val threadRootId: ThreadId) : PinnedMessagesListEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt index a3728cb9f4..9633802f77 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNavigator.kt @@ -9,10 +9,12 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo interface PinnedMessagesListNavigator { fun viewInTimeline(eventId: EventId) fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun forwardEvent(eventId: EventId) + fun navigateToThread(threadRootId: ThreadId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 292a77ba6a..cddc1831db 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser @@ -55,6 +56,7 @@ class PinnedMessagesListNode( fun handlePermalinkClick(data: PermalinkData.RoomLink) fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) fun handleForwardEventClick(eventId: EventId) + fun navigateToThread(threadRootId: ThreadId) } private val callback: Callback = callback() @@ -95,6 +97,10 @@ class PinnedMessagesListNode( callback.handleForwardEventClick(eventId) } + override fun navigateToThread(threadRootId: ThreadId) { + callback.navigateToThread(threadRootId) + } + @Composable override fun View(modifier: Modifier) { CompositionLocalProvider( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index f884cdac84..6cd037484d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -137,6 +137,7 @@ class PinnedMessagesListPresenter( fun handleEvent(event: PinnedMessagesListEvent) { when (event) { is PinnedMessagesListEvent.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event) + is PinnedMessagesListEvent.OpenThread -> navigator.navigateToThread(event.threadRootId) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index b125bdcf6f..b212549a22 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.link.LinkEvent import io.element.android.features.messages.impl.link.LinkView +import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.components.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData @@ -235,7 +236,12 @@ private fun PinnedMessagesListLoaded( onReadReceiptClick = {}, onSwipeToReply = {}, onJoinCallClick = {}, - eventSink = {}, + eventSink = { timelineItemEvent -> + when (timelineItemEvent) { + is TimelineEvent.OpenThread -> state.eventSink(PinnedMessagesListEvent.OpenThread(timelineItemEvent.threadRootEventId)) + else -> Unit + } + }, eventContentView = { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentViewWrapper( event = event, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index e1c053f259..0af0fbb9f5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -112,7 +112,7 @@ class ThreadedMessagesNode( this.timelineController = timelineController return presenterFactory.create( navigator = this, - composerPresenter = messageComposerPresenterFactory.create(timelineController, this), + composerPresenter = messageComposerPresenterFactory.create(timelineController, this, isInThread = true), timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), // TODO add special processor for threaded timeline actionListPresenter = actionListPresenterFactory.create( @@ -136,6 +136,7 @@ class ThreadedMessagesNode( fun navigateToEditPoll(eventId: EventId) fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) + fun navigateToDeveloperSettings() fun navigateToPaymentFlow(roomId: RoomId, recipientUserId: UserId?, recipientAddress: String?, amountLovelace: Long?) } @@ -234,10 +235,18 @@ class ThreadedMessagesNode( callback.handlePermalinkClick(permalinkData) } + override fun navigateToMember(userId: UserId) { + callback.navigateToRoomMemberDetails(userId) + } + override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { callback.navigateToThread(threadRootId, focusedEventId) } + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } + override fun navigateToPaymentFlow( roomId: RoomId, recipientUserId: UserId?, @@ -302,6 +311,7 @@ class ThreadedMessagesNode( onViewAllPinnedMessagesClick = {}, modifier = modifier, knockRequestsBannerView = {}, + onThreadsListClick = {}, ) roomMemberModerationRenderer.Render( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadListRowItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadListRowItem.kt new file mode 100644 index 0000000000..3380a32f61 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadListRowItem.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.threads.list + +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem + +data class ThreadListRowItem( + val item: ThreadListItem, + val rootEventText: String?, + val latestEventText: String?, + val formattedTimestamp: String, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListNode.kt new file mode 100644 index 0000000000..1954dc60a5 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListNode.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.threads.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 dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.ThreadId + +@ContributesNode(RoomScope::class) +@AssistedInject +class ThreadsListNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ThreadsListPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openThread(threadId: ThreadId) + } + + private val callback: Callback = callback() + + @Composable + override fun View(modifier: Modifier) { + ThreadsListView( + state = presenter.present(), + modifier = modifier, + onThreadClick = callback::openThread, + onBackClick = this::navigateUp, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt new file mode 100644 index 0000000000..9d15376e9f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.threads.list + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory +import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.api.DateFormatterMode +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import timber.log.Timber + +@Inject +class ThreadsListPresenter( + private val room: JoinedRoom, + private val timelineItemContentFactory: TimelineItemContentFactory, + private val messageSummaryFormatter: MessageSummaryFormatter, + private val dateFormatter: DateFormatter, +) : Presenter { + @Composable + override fun present(): ThreadsListState { + val coroutineScope = rememberCoroutineScope() + val threadsListService = room.threadsListService + + val threads by produceState(initialValue = persistentListOf(), key1 = threadsListService) { + threadsListService.subscribeToItemUpdates() + .onStart { threadsListService.paginate() } + .collect { items -> + Timber.d("Received thread list update with ${items.size} items") + value = items.map { item -> + val rootTimelineEvent = item.rootEvent.content?.let { + timelineItemContentFactory.create( + itemContent = it, + eventId = item.rootEvent.eventId, + isEditable = false, + sender = item.rootEvent.senderId, + senderProfile = item.rootEvent.senderProfile, + ) + } + val rootEventText = rootTimelineEvent?.let { messageSummaryFormatter.format(it) } + + val latestTimelineEvent = item.latestEvent?.content?.let { + timelineItemContentFactory.create( + itemContent = it, + eventId = item.latestEvent!!.eventId, + isEditable = false, + sender = item.latestEvent!!.senderId, + senderProfile = item.latestEvent!!.senderProfile, + ) + } + val latestEventText = latestTimelineEvent?.let { messageSummaryFormatter.format(it) } + + val formattedTimestamp = dateFormatter.format( + timestamp = item.latestEvent?.timestamp ?: item.rootEvent.timestamp, + mode = DateFormatterMode.TimeOrDate, + useRelative = true, + ) + + ThreadListRowItem( + item = item, + rootEventText = rootEventText, + latestEventText = latestEventText, + formattedTimestamp = formattedTimestamp, + ) + }.toImmutableList() + } + } + + val paginationStatus by produceState( + initialValue = ThreadListPaginationStatus.Idle(hasMoreToLoad = true), + key1 = threadsListService + ) { + threadsListService + .subscribeToPaginationUpdates() + .collect { value = it } + } + + val roomInfo by room.roomInfoFlow.collectAsState() + + DisposableEffect(Unit) { + onDispose { + threadsListService.destroy() + } + } + + fun handleEvent(event: ThreadsListEvents) { + when (event) { + ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) { + coroutineScope.launch { + Timber.d("Paginating thread list: $paginationStatus") + threadsListService.paginate() + } + } else { + Timber.d("Not paginating since there is nothing else to load, current status: $paginationStatus") + } + } + } + + return ThreadsListState( + threads = threads, + roomId = room.roomId, + roomName = roomInfo.name ?: room.roomId.value, + roomAvatarUrl = roomInfo.avatarUrl, + isRoomTombstoned = roomInfo.successorRoom != null, + eventSink = ::handleEvent, + ) + } +} + +data class ThreadsListState( + val roomId: RoomId, + val roomName: String, + val roomAvatarUrl: String?, + val isRoomTombstoned: Boolean, + val threads: ImmutableList, + val eventSink: (ThreadsListEvents) -> Unit, +) + +sealed interface ThreadsListEvents { + data object Paginate : ThreadsListEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt new file mode 100644 index 0000000000..c93af5c162 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.threads.list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.asEventId +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem +import io.element.android.libraries.matrix.api.room.threads.ThreadListItemEvent +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl +import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ThreadsListView( + state: ThreadsListState, + onThreadClick: (ThreadId) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Avatar( + avatarData = AvatarData( + id = state.roomId.value, + name = state.roomName, + url = state.roomAvatarUrl, + size = AvatarSize.CurrentUserTopBar, + ), + avatarType = AvatarType.Room(isTombstoned = state.isRoomTombstoned), + contentDescription = null, + ) + Column { + Text( + text = stringResource(CommonStrings.common_threads), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = state.roomName, + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + }, + navigationIcon = { + BackButton(onBackClick) + } + ) + } + ) { padding -> + val lazyListState = rememberLazyListState() + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = padding, + state = lazyListState, + ) { + itemsIndexed(state.threads, key = { _, row -> row.item.threadId }) { index, row -> + ThreadListItemRow( + threadItem = row, + onClick = onThreadClick, + ) + + if (index < state.threads.size - 1) { + HorizontalDivider() + } + } + } + + ScrollHelper(lazyListState) { + state.eventSink(ThreadsListEvents.Paginate) + } + } +} + +@Composable +private fun ScrollHelper( + listState: LazyListState, + onPaginate: () -> Unit, +) { + val lastVisibleItemIndex by remember { + derivedStateOf { listState.firstVisibleItemIndex + listState.layoutInfo.visibleItemsInfo.size - 1 } + } + val needsPagination by remember { + derivedStateOf { + val canLoadNewItems = listState.isScrollInProgress || listState.firstVisibleItemScrollOffset == 0 + canLoadNewItems && lastVisibleItemIndex == listState.layoutInfo.totalItemsCount - 1 + } + } + LaunchedEffect(needsPagination, lastVisibleItemIndex) { + if (needsPagination) { + onPaginate() + delay(400L) + } + } +} + +@Composable +private fun ThreadListItemRow( + threadItem: ThreadListRowItem, + onClick: (ThreadId) -> Unit, +) { + Row( + modifier = Modifier + .clickable { onClick(threadItem.item.threadId) } + .fillMaxWidth() + .padding(top = 4.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + ) { + val rootEvent = threadItem.item.rootEvent + val senderProfile = rootEvent.senderProfile + Avatar( + modifier = Modifier.align(Alignment.CenterVertically), + avatarData = AvatarData( + id = rootEvent.senderId.value, + name = senderProfile.getDisambiguatedDisplayName(rootEvent.senderId), + url = senderProfile.getAvatarUrl(), + size = AvatarSize.ThreadsListItem, + ), + avatarType = AvatarType.User, + contentDescription = null, + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + // TODO actually compute these values based on the thread state (not available yet) + val hasMentions = false + val hasUnreadNotifications = false + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = senderProfile.getDisambiguatedDisplayName(rootEvent.senderId), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = threadItem.formattedTimestamp, + style = ElementTheme.typography.fontBodySmRegular, + color = if (hasUnreadNotifications || hasMentions) ElementTheme.colors.textActionAccent else ElementTheme.colors.textSecondary, + ) + } + + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = threadItem.rootEventText.orEmpty(), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(7.dp) + ) { + if (hasMentions) { + Icon( + modifier = Modifier.size(14.dp), + imageVector = CompoundIcons.Mention(), + contentDescription = null, + tint = ElementTheme.colors.textActionAccent, + ) + } + + UnreadIndicatorAtom( + size = 14.dp, + isVisible = hasUnreadNotifications, + ) + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${threadItem.item.numberOfReplies}", + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Icon( + modifier = Modifier.size(20.dp), + imageVector = CompoundIcons.ThreadsSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary + ) + + Spacer(modifier = Modifier.width(8.dp)) + + threadItem.item.latestEvent?.let { latestEvent -> + Avatar( + avatarData = AvatarData( + id = latestEvent.senderId.value, + name = latestEvent.senderProfile.getDisambiguatedDisplayName(latestEvent.senderId), + url = latestEvent.senderProfile.getAvatarUrl(), + size = AvatarSize.TimelineThreadLatestEventSender, + ), + avatarType = AvatarType.User, + contentDescription = null, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = threadItem.latestEventText.orEmpty(), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun ThreadsListViewPreview() { + ElementPreview { + ThreadsListView( + state = ThreadsListState( + roomId = RoomId("!room-id:server"), + roomName = "Room name", + roomAvatarUrl = null, + threads = List(10) { aThreadListRowItem(threadId = ThreadId("\$thread-$it")) }.toImmutableList(), + isRoomTombstoned = false, + eventSink = {}, + ), + onThreadClick = {}, + onBackClick = {}, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun ThreadListItemRowPreview() { + ElementPreview { + ThreadListItemRow( + threadItem = aThreadListRowItem(), + onClick = {}, + ) + } +} + +fun aThreadListRowItem( + threadId: ThreadId = ThreadId("\$a-thread-id"), + rootEvent: ThreadListItemEvent = aThreadListItemEvent(threadId = threadId), + latestEvent: ThreadListItemEvent? = aThreadListItemEvent(threadId = threadId), + numberOfReplies: Long = 42, + rootEventText: String? = "Hello world!", + latestEventText: String? = "Hello again!", + formattedTimestamp: String = "12:34", +) = ThreadListRowItem( + item = aThreadListItem( + threadId = threadId, + rootEvent = rootEvent, + latestEvent = latestEvent, + numberOfReplies = numberOfReplies, + ), + rootEventText = rootEventText, + latestEventText = latestEventText, + formattedTimestamp = formattedTimestamp, +) + +fun aThreadListItem( + threadId: ThreadId = ThreadId("\$a-thread-id"), + rootEvent: ThreadListItemEvent = aThreadListItemEvent(threadId = threadId), + latestEvent: ThreadListItemEvent? = aThreadListItemEvent(threadId = threadId), + numberOfReplies: Long = 42, +) = ThreadListItem( + rootEvent = rootEvent, + latestEvent = latestEvent, + numberOfReplies = numberOfReplies, +) + +fun aThreadListItemEvent( + threadId: ThreadId = ThreadId("\$a-thread-id"), + senderId: UserId = UserId("@a-user-id:server"), + senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = "Alice", displayNameAmbiguous = false, avatarUrl = null), + isOwn: Boolean = false, + content: EventContent = MessageContent( + body = "Hello world!", + inReplyTo = null, + isEdited = false, + threadInfo = null, + type = TextMessageType("Hello world!", null), + ), + timestamp: Long = 0L, +) = ThreadListItemEvent( + eventId = threadId.asEventId(), + senderId = senderId, + senderProfile = senderProfile, + isOwn = isOwn, + content = content, + timestamp = timestamp, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 12e4e0b1d1..8a7011552e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -149,6 +149,9 @@ class TimelinePresenter( val displayThreadSummaries by produceState(false) { value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) } + val displayFloatingDateBadge by produceState(false) { + value = featureFlagService.isFeatureEnabled(FeatureFlags.FloatingDateBadge) + } fun handleEvent(event: TimelineEvent) { when (event) { @@ -315,6 +318,7 @@ class TimelinePresenter( messageShieldDialogData = messageShieldDialogData.value, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, displayThreadSummaries = displayThreadSummaries, + displayFloatingDateBadge = displayFloatingDateBadge, eventSink = ::handleEvent, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 03f0083856..1869ad6906 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -34,6 +34,7 @@ data class TimelineState( val messageShieldDialogData: MessageShieldData?, val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState, val displayThreadSummaries: Boolean, + val displayFloatingDateBadge: Boolean, val eventSink: (TimelineEvent) -> Unit, ) { private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 184acf1386..9840ac5107 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -56,6 +56,7 @@ fun aTimelineState( messageShield: MessageShield? = null, resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(), displayThreadSummaries: Boolean = false, + displayFloatingDateBadge: Boolean = false, eventSink: (TimelineEvent) -> Unit = {}, ): TimelineState { val focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId @@ -75,6 +76,7 @@ fun aTimelineState( messageShieldDialogData = messageShield?.let { MessageShieldData(it) }, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, displayThreadSummaries = displayThreadSummaries, + displayFloatingDateBadge = displayFloatingDateBadge, eventSink = eventSink, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 08a7191f3f..0c5bb28890 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -47,10 +47,12 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView +import io.element.android.features.messages.impl.timeline.components.FloatingDateBadgeOverlay import io.element.android.features.messages.impl.timeline.components.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.toText import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories @@ -82,6 +84,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import timber.log.Timber @@ -105,6 +108,7 @@ fun TimelineView( lazyListState: LazyListState = rememberLazyListState(), forceJumpToBottomVisibility: Boolean = false, nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(), + floatingDateTopOffset: Dp = 0.dp, ) { fun clearFocusRequestState() { state.eventSink(TimelineEvent.ClearFocusRequestState) @@ -210,6 +214,15 @@ fun TimelineView( onJumpToLive = ::onJumpToLive, onFocusEventRender = ::onFocusEventRender, ) + + if (state.displayFloatingDateBadge && useReverseLayout) { + FloatingDateBadgeOverlay( + lazyListState = lazyListState, + timelineItems = state.timelineItems, + isLive = state.isLive, + topOffset = floatingDateTopOffset, + ) + } } } @@ -250,11 +263,16 @@ private fun TimelinePrefetchingHelper( firstVisibleItemIndex + layoutInfo.visibleItemsInfo.size >= layoutInfo.totalItemsCount - 40 } + // If we have no timeline items, we need to back paginate to load some messages. This usually happens on all timelines except for live ones. + // This automatic pagination was previously done by the SDK, and we received a `Reset` update, but now we need to do it ourselves. + val isEmptyTimelineFlow = layoutInfoFlow.map { it.totalItemsCount == 0 } + combine( isCloseToStartOfLoadedTimelineFlow.distinctUntilChanged(), isScrollingFlow.distinctUntilChanged(), - ) { needsPrefetch, isScrolling -> - needsPrefetch && isScrolling + isEmptyTimelineFlow, + ) { needsPrefetch, isScrolling, isEmptyAndNeedsBackPagination -> + isEmptyAndNeedsBackPagination || needsPrefetch && isScrolling } .distinctUntilChanged() .collectLatest { needsPrefetch -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt new file mode 100644 index 0000000000..996bb07b81 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/FloatingDateBadge.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.floatingDateBadgeBackground +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlin.time.Duration.Companion.milliseconds + +@Composable +internal fun BoxScope.FloatingDateBadgeOverlay( + lazyListState: LazyListState, + timelineItems: ImmutableList, + isLive: Boolean, + topOffset: Dp = 0.dp, +) { + // This needs to be a state to trigger a `derivedState` recalculation + val updatedTimelineItems by rememberUpdatedState(timelineItems) + + // Look for the last visible item with a timestamp, starting from the last visible item and going backwards until we find one or reach the start of the list + val lastVisibleItemWithTimestamp by remember { + derivedStateOf { + var index = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf null + while (index >= 0) { + when (val item = updatedTimelineItems.getOrNull(index)) { + is TimelineItem.Event -> return@derivedStateOf item + is TimelineItem.Virtual -> if (item.model is TimelineItemDaySeparatorModel) return@derivedStateOf item + is TimelineItem.GroupedEvents -> return@derivedStateOf item.events.firstOrNull() + null -> Unit + } + index-- + } + null + } + } + + // Store the formatted date so we recompute it lazily and can keep it around even if we need to dispose the badge because the timeline items changed + var formattedDate: String? by remember { mutableStateOf(null) } + // Update the formatted date when we have a new non-null timestamp + LaunchedEffect(lastVisibleItemWithTimestamp) { + lastVisibleItemWithTimestamp?.formattedDate()?.let { formattedDate = it } + } + + val isAtBottom by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex < 3 && isLive + } + } + + var isBadgeVisible by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + snapshotFlow { lazyListState.isScrollInProgress } + .collectLatest { isScrolling -> + if (isScrolling) { + isBadgeVisible = true + } else { + delay(2000.milliseconds) + isBadgeVisible = false + } + } + } + + val showBadge = isBadgeVisible && !isAtBottom && formattedDate != null + + AnimatedVisibility( + visible = showBadge, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 8.dp + topOffset), + enter = fadeIn(animationSpec = tween(150)), + exit = fadeOut(animationSpec = tween(300)), + ) { + formattedDate?.let { dateText -> + FloatingDateBadge( + modifier = Modifier.padding(8.dp), + dateText = dateText, + ) + } + } +} + +@Composable +internal fun FloatingDateBadge( + dateText: String, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + color = ElementTheme.colors.floatingDateBadgeBackground, + shadowElevation = 4.dp, + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = dateText, + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textPrimary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun FloatingDateBadgePreview() = ElementPreview { + Box(modifier = Modifier.padding(16.dp)) { + FloatingDateBadge(dateText = "March 9, 2026") + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index c9bc130905..0361eebc01 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -747,7 +747,7 @@ private fun MessageEventBubbleContent( } Box( modifier = talkbackCompatModifier - .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(6.dp)) + .border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp)) .background(ElementTheme.colors.bgCanvasDefault, RoundedCornerShape(6.dp)) .padding(4.dp) ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 366c88157e..cf515a0b51 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -66,6 +66,11 @@ class TimelineItemEventFactory( timestamp = currentTimelineItem.event.timestamp, mode = DateFormatterMode.TimeOnly, ) + val sentDate = dateFormatter.format( + timestamp = currentTimelineItem.event.timestamp, + mode = DateFormatterMode.Day, + useRelative = true, + ) val senderAvatarData = AvatarData( id = currentSender.value, name = senderProfile.getDisambiguatedDisplayName(currentSender), @@ -108,6 +113,7 @@ class TimelineItemEventFactory( canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo, sentTimeMillis = currentTimelineItem.event.timestamp, sentTime = sentTime, + sentDate = sentDate, groupPosition = groupPosition, reactionsState = currentTimelineItem.computeReactionsState(), readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index e169b10403..c9adba21da 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId @@ -59,6 +60,12 @@ sealed interface TimelineItem { is GroupedEvents -> "groupedEvent" } + fun formattedDate(): String? = when (this) { + is Event -> sentDate.takeIf { it.isNotEmpty() } + is Virtual -> (model as? TimelineItemDaySeparatorModel)?.formattedDate?.takeIf { it.isNotEmpty() } + is GroupedEvents -> null + } + data class Virtual( val id: UniqueId, val model: TimelineItemVirtualModel @@ -75,6 +82,7 @@ sealed interface TimelineItem { val content: TimelineItemEventContent, val sentTimeMillis: Long = 0L, val sentTime: String = "", + val sentDate: String = "", val isMine: Boolean = false, val isEditable: Boolean, val canBeRepliedTo: Boolean, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt index af5a5fffa3..f54f93123d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -12,10 +12,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -30,8 +29,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.MessagesMenuActions import io.element.android.features.messages.impl.SharedHistoryIcon -import io.element.android.features.messages.impl.timeline.components.CallMenuItem import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.features.roomcall.api.anOngoingCallState @@ -45,7 +44,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -63,15 +61,12 @@ internal fun MessagesViewTopBar( roomAvatar: AvatarData, isTombstoned: Boolean, heroes: ImmutableList, - roomCallState: RoomCallState, dmUserIdentityState: IdentityState?, sharedHistoryIcon: SharedHistoryIcon, - isDmRoom: Boolean, onRoomDetailsClick: () -> Unit, - onJoinCallClick: (isAudioCall: Boolean) -> Unit, - onWalletClick: () -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, + menuActions: @Composable RowScope.() -> Unit, ) { TopAppBar( modifier = modifier, @@ -129,22 +124,7 @@ internal fun MessagesViewTopBar( } } }, - actions = { - // Wallet button - only show in DM rooms - if (isDmRoom) { - IconButton(onClick = onWalletClick) { - Icon( - imageVector = CompoundIcons.Chart(), - contentDescription = "Cardano Wallet", - ) - } - } - CallMenuItem( - roomCallState = roomCallState, - onJoinCallClick = onJoinCallClick, - ) - Spacer(Modifier.width(8.dp)) - }, + actions = menuActions, windowInsets = WindowInsets(0.dp) ) } @@ -199,19 +179,26 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { dmUserIdentityState: IdentityState? = null, sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, isDmRoom: Boolean = false, + displayThreads: Boolean = false, ) = MessagesViewTopBar( roomName = roomName, roomAvatar = roomAvatar, isTombstoned = isTombstoned, heroes = heroes, - roomCallState = roomCallState, dmUserIdentityState = dmUserIdentityState, sharedHistoryIcon = sharedHistoryIcon, - isDmRoom = isDmRoom, onRoomDetailsClick = {}, - onJoinCallClick = {}, - onWalletClick = {}, onBackClick = {}, + menuActions = { + MessagesMenuActions( + roomCallState = roomCallState, + displayThreads = displayThreads, + onJoinCallClick = {}, + onThreadsListClick = {}, + isDmRoom = isDmRoom, + onWalletClick = {}, + ) + } ) Column { AMessagesViewTopBar() @@ -253,5 +240,9 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { roomName = "A room with world_readable history", sharedHistoryIcon = SharedHistoryIcon.WORLD_READABLE, ) + HorizontalDivider() + AMessagesViewTopBar( + displayThreads = true, + ) } } diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml index 0d750e7ed5..4eb3fd61bb 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -35,7 +35,7 @@ "Registra video" "Allegato" "Libreria di foto e video" - "Posizione" + "Condividi posizione" "Sondaggio" "Formattazione del testo" "La cronologia dei messaggi non è attualmente disponibile." diff --git a/features/messages/impl/src/main/res/values-ja/translations.xml b/features/messages/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..4fd18e8db5 --- /dev/null +++ b/features/messages/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,81 @@ + + + "イベントの送信者と、使用された端末の所有者が一致しません。" + "この暗号化されたメッセージの真正性を、この端末では保証できません。" + "以前に検証されたユーザーにより暗号化されています。" + "暗号化されていません。" + "削除されたまたは不明な端末により暗号化されています。" + "所有者に検証されていない端末により暗号化されています。" + "未検証のユーザーにより暗号化されています。" + "アクティビティ" + "旗" + "食べ物" + "動物・自然" + "物" + "顔・人" + "旅・場所" + "最近使用" + "記号" + "古いアプリケーションを使用しているユーザーはキャプションを見られない可能性があります。" + "動画のアップロード画質を変更するにはタップしてください" + "ファイルをアップロードに失敗しました。" + "ファイルの処理に失敗しました。再試行してください。" + "ファイルのアップロードに失敗しました。再試行してください。" + "許容されている最大サイズは %1$s です。" + "ファイルが大きすぎるためアップロードできません" + "個数 %1$d / %2$d" + "画像の品質を最適化" + "処理中…" + "ユーザーをブロック" + "このユーザーからのメッセージをすべて非表示にする場合はチェックしてください。" + "このメッセージはホームサーバーの管理者に報告されます。暗号化されたメッセージを確認することはできません。" + "このコンテンツを通報する理由" + "カメラ" + "写真を撮影" + "動画を撮影" + "添付ファイル" + "アルバムの写真・動画" + "場所を共有" + "投票" + "書式設定" + "過去のメッセージを現在表示できません。" + "このルームの過去のメッセージを表示できません。確認するには、この端末を検証してください。" + "招待し直しますか?" + "このチャットにはあなた一人だけです" + "ルーム全体に通知" + "全員" + "再送信する" + "メッセージの送信に失敗しました" + "リアクションを追加" + "%1$s の始まりです。" + "ここが会話の開始点です。" + "非対応の着信です。新しい Element X を使用できないか確認してください。" + "一部を表示" + "メッセージをコピーしました" + "このルームに発言する権限がありません" + + "%1$d 人の反応 %2$s" + + + "あなたと %1$d 人の反応 %2$s" + + "%1$s と反応" + "一部を表示" + "さらに表示" + "リアクションのまとめを表示" + "新着" + + "%1$d 個のルーム更新点" + + "新しいルームに移動" + "このルームは移行して非アクティブ状態です" + "古いメッセージを表示" + "このルームは他のルームからの移行先です" + + "%1$s, %2$s 他 %3$d 人" + + + "%1$s が入力中" + + "%1$s と %2$s" + diff --git a/features/messages/impl/src/main/res/values-sv/translations.xml b/features/messages/impl/src/main/res/values-sv/translations.xml index 21d2cd5fe2..2e10fd3166 100644 --- a/features/messages/impl/src/main/res/values-sv/translations.xml +++ b/features/messages/impl/src/main/res/values-sv/translations.xml @@ -33,7 +33,7 @@ "Spela in video" "Bilaga" "Foto- och videobibliotek" - "Plats" + "Dela plats" "Omröstning" "Textformatering" "Meddelandehistoriken är för närvarande otillgänglig." diff --git a/features/messages/impl/src/main/res/values-vi/translations.xml b/features/messages/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..81ac0b7b8e --- /dev/null +++ b/features/messages/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,60 @@ + + + "Người gửi sự kiện không khớp với chủ sở hữu của thiết bị đã gửi nó." + "Tin nhắn mã hóa này không thể được xác thực trên thiết bị này." + "Được mã hóa bởi một người dùng đã từng được xác minh." + "Không được mã hóa" + "Được mã hóa bởi một thiết bị không xác định hoặc đã bị xóa." + "Được mã hóa bởi một thiết bị chưa được chủ sở hữu xác minh." + "Được mã hóa bởi một người dùng chưa được xác minh." + "Hoạt động" + "Cờ" + "Thực phẩm và đồ uống" + "Động vật và thiên nhiên" + "Đồ vật" + "Mặt cười & mọi người" + "Du lịch và địa danh" + "Biểu tượng" + "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." + "Không thể tải lên tệp phương tiện. Vui lòng thử lại." + "Chặn người dùng" + "Chọn tùy chọn này nếu bạn muốn ẩn tất cả tin nhắn hiện tại và tương lai từ người dùng này." + "Tin nhắn này sẽ được báo cáo cho quản trị viên máy chủ của bạn. Họ sẽ không thể đọc bất kỳ tin nhắn được mã hóa nào." + "Lý do báo cáo nội dung này" + "Máy ảnh" + "Chụp ảnh" + "Quay video" + "Tệp đính kèm" + "Thư viện ảnh và video" + "Chia sẻ vị trí" + "Bỏ phiếu" + "Định dạng văn bản" + "Lịch sử tin nhắn hiện không khả dụng." + "Lịch sử tin nhắn không khả dụng trong phòng này. Vui lòng xác minh thiết bị này để xem lịch sử tin nhắn của bạn." + "Bạn có muốn mời họ quay lại không?" + "Bạn đang một mình trong cuộc trò chuyện này" + "Thông báo cho cả phòng" + "Mọi người" + "Gửi lại" + "Gửi tin nhắn không thành công" + "Thêm biểu cảm" + "Đây là sự kiện khởi đầu của phòng %1$s ." + "Đây là khởi đầu của cuộc trò chuyện này." + "Cuộc gọi không được hỗ trợ. Hãy hỏi xem người gọi có thể sử dụng ứng dụng Element X mới hay không." + "Thu gọn" + "Đã sao chép tin nhắn" + "Bạn không có quyền gửi tin nhắn trong phòng này" + "Thu gọn" + "Xem thêm" + "Mới" + + "%1$d số lượng phòng thay đổi" + + + "%1$s,%2$s và %3$d người khác" + + + "%1$s đang gõ" + + "%1$s và %2$s" + diff --git a/features/messages/impl/src/main/res/values-zh/translations.xml b/features/messages/impl/src/main/res/values-zh/translations.xml index 5247193b84..2a6b9bf78d 100644 --- a/features/messages/impl/src/main/res/values-zh/translations.xml +++ b/features/messages/impl/src/main/res/values-zh/translations.xml @@ -26,7 +26,7 @@ "第%1$d/%2$d项" "优化图像质量" "处理中…" - "封禁用户" + "屏蔽用户" "请确认是否要隐藏该用户当前和未来的所有信息" "此消息将举报给您的服务器管理员。他们无法读取任何加密消息。" "举报此内容的原因" @@ -35,13 +35,13 @@ "录制视频" "附件" "照片和视频库" - "位置" + "共享位置" "投票" "文本格式化" "消息历史记录当前不可用。" "此聊天室无法查看消息历史记录。请验证此设备以查看之。" "您想邀请他们回来吗?" - "聊天中只有你一个人" + "此聊天室中只有您一个人" "通知整个聊天室" "所有人" "再次发送" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt index a1db09dfda..dc50fca2c3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt @@ -93,6 +93,7 @@ class DefaultMessagesEntryPointTest { override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() override fun navigateToRoom(roomId: RoomId) = lambdaError() + override fun navigateToDeveloperSettings() = lambdaError() } val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID) val params = MessagesEntryPoint.Params(initialTarget) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt index 68d2cd824b..44d82f1a7c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/FakeMessagesNavigator.kt @@ -24,6 +24,8 @@ class FakeMessagesNavigator( private val onEditPollClickLambda: (eventId: EventId) -> Unit = { _ -> lambdaError() }, private val onPreviewAttachmentLambda: (attachments: ImmutableList, inReplyToEventId: EventId?) -> Unit = { _, _ -> lambdaError() }, private val onNavigateToRoomLambda: (roomId: RoomId, threadId: EventId?, serverNames: List) -> Unit = { _, _, _ -> lambdaError() }, + private val navigateToMemberLambda: (userId: UserId) -> Unit = { lambdaError() }, + private val navigateToDeveloperSettingsLambda: () -> Unit = { lambdaError() }, private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() }, private val closeLambda: () -> Unit = { lambdaError() }, ) : MessagesNavigator { @@ -51,10 +53,18 @@ class FakeMessagesNavigator( onNavigateToRoomLambda(roomId, eventId, serverNames) } + override fun navigateToMember(userId: UserId) { + navigateToMemberLambda(userId) + } + override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { onOpenThreadLambda(threadRootId, focusedEventId) } + override fun navigateToDeveloperSettings() { + navigateToDeveloperSettingsLambda() + } + override fun close() { closeLambda() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index f6967de0e5..6e12c607d8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState +import io.element.android.features.messages.impl.threads.list.aThreadListItem import io.element.android.features.messages.impl.timeline.FakeMarkAsFullyRead import io.element.android.features.messages.impl.timeline.MarkAsFullyRead import io.element.android.features.messages.impl.timeline.TimelineController @@ -88,6 +89,7 @@ import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions +import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aTimelineItemDebugInfo import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails @@ -110,6 +112,7 @@ import io.element.android.tests.testutils.testWithLifecycleOwner import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent @@ -1258,6 +1261,35 @@ class MessagesPresenterTest { } } + @Test + fun `present - only has threads enabled if the feature flag is on`() = runTest { + val itemsFlow = MutableStateFlow(listOf(aThreadListItem())) + val room = FakeJoinedRoom( + threadsListService = FakeThreadsListService(items = itemsFlow) + ) + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.Threads.key to false) + ) + val presenter = createMessagesPresenter( + joinedRoom = room, + featureFlagService = featureFlagService + ) + presenter.testWithLifecycleOwner { + val initialState = awaitItem() + // The feature flag is disabled, so even if the thread list has items, it will return it doesn't have any + assertThat(initialState.threads.hasThreads).isFalse() + + // Enable the feature flag, now it should reflect the thread list state + featureFlagService.setFeatureEnabled(FeatureFlags.RoomThreadList, true) + skipItems(1) + assertThat(awaitItem().threads.hasThreads).isTrue() + + // And if we remove the items, it should update accordingly + itemsFlow.value = emptyList() + assertThat(awaitItem().threads.hasThreads).isFalse() + } + } + private fun roomPermissions( canStartCall: Boolean = true, canRedactOther: Boolean = true, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index c78aa39265..62b9eac68d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -73,6 +73,7 @@ import io.element.android.tests.testutils.assertNoNodeWithText import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.setSafeContent import kotlinx.collections.immutable.persistentListOf @@ -521,6 +522,9 @@ class MessagesViewTest { rule.setMessagesView( state = stateWithActionListState, ) + // Clear initial 'LoadMore' event emitted when setting the state + eventsRecorder.clear() + val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") rule.onNodeWithText(verifiedUserSendFailure).performClick() // Give time for the close animation to complete @@ -584,6 +588,9 @@ class MessagesViewTest { ), ) rule.setMessagesView(state = state) + // Clear initial 'LoadMore' event emitted when setting the state + eventsRecorder.clear() + rule.onNodeWithText("This is a pinned message").performClick() eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) } @@ -600,12 +607,32 @@ class MessagesViewTest { timelineState = aTimelineState(eventSink = eventsRecorder) ) rule.setMessagesView(state = state) + // Clear initial 'LoadMore' event emitted when setting the state + eventsRecorder.clear() + val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action) // The bottomsheet subcompose seems to make the node to appear twice rule.onAllNodesWithText(text).onFirst().performClick() eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) } + @Test + fun `clicking on threads list button calls the expected function`() { + val state = aMessagesState( + threads = MessagesState.Threads( + hasThreads = true, + hasUnreadThreads = false, + ) + ) + val onThreadsListClicked = lambdaRecorder {} + rule.setMessagesView( + state = state, + onThreadsListClicked = onThreadsListClicked, + ) + rule.onNodeWithContentDescription("Threads").performClick() + onThreadsListClicked.assertions().isCalledOnce() + } + @Test fun `no banner shown when there is no successor room`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -630,6 +657,7 @@ private fun AndroidComposeTestRule.setMessa onCreatePollClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), + onThreadsListClicked: () -> Unit = EnsureNeverCalled(), ) { setSafeContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode @@ -646,6 +674,7 @@ private fun AndroidComposeTestRule.setMessa onJoinCallClick = onJoinCallClick, onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, knockRequestsBannerView = {}, + onThreadsListClick = onThreadsListClicked, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index c7eb7c0bce..2f87af3df0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -48,38 +48,42 @@ internal fun TestScope.aTimelineItemsFactoryCreator(): TimelineItemsFactory.Crea } } +internal fun aTimelineItemContentFactory( + timelineEventFormatter: TimelineEventFormatter = aTimelineEventFormatter(), + matrixClient: FakeMatrixClient = FakeMatrixClient(), +): TimelineItemContentFactory = TimelineItemContentFactory( + messageFactory = TimelineItemContentMessageFactory( + fileSizeFormatter = FakeFileSizeFormatter(), + fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), + htmlConverterProvider = FakeHtmlConverterProvider(), + permalinkParser = FakePermalinkParser(), + textPillificationHelper = FakeTextPillificationHelper(), + ), + redactedMessageFactory = TimelineItemContentRedactedFactory(), + stickerFactory = TimelineItemContentStickerFactory( + fileSizeFormatter = FakeFileSizeFormatter(), + fileExtensionExtractor = FileExtensionExtractorWithoutValidation() + ), + pollFactory = TimelineItemContentPollFactory(FakePollContentStateFactory()), + utdFactory = TimelineItemContentUTDFactory(), + roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), + profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), + stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), + failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), + failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), + sessionId = matrixClient.sessionId, +) + internal fun TestScope.aTimelineItemsFactory( config: TimelineItemsFactoryConfig, ): TimelineItemsFactory { - val timelineEventFormatter = aTimelineEventFormatter() val matrixClient = FakeMatrixClient() return TimelineItemsFactory( dispatchers = testCoroutineDispatchers(), eventItemFactoryCreator = object : TimelineItemEventFactory.Creator { override fun create(config: TimelineItemsFactoryConfig): TimelineItemEventFactory { return TimelineItemEventFactory( - contentFactory = TimelineItemContentFactory( - messageFactory = TimelineItemContentMessageFactory( - fileSizeFormatter = FakeFileSizeFormatter(), - fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), - htmlConverterProvider = FakeHtmlConverterProvider(), - permalinkParser = FakePermalinkParser(), - textPillificationHelper = FakeTextPillificationHelper(), - ), - redactedMessageFactory = TimelineItemContentRedactedFactory(), - stickerFactory = TimelineItemContentStickerFactory( - fileSizeFormatter = FakeFileSizeFormatter(), - fileExtensionExtractor = FileExtensionExtractorWithoutValidation() - ), - pollFactory = TimelineItemContentPollFactory(FakePollContentStateFactory()), - utdFactory = TimelineItemContentUTDFactory(), - roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), - profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), - stateFactory = TimelineItemContentStateFactory(timelineEventFormatter), - failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), - failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), - sessionId = matrixClient.sessionId, - ), + contentFactory = aTimelineItemContentFactory(matrixClient = matrixClient), matrixClient = matrixClient, dateFormatter = FakeDateFormatter(), permalinkParser = FakePermalinkParser(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt new file mode 100644 index 0000000000..116a1cfb5d --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.impl.messagecomposer + +import android.net.Uri +import app.cash.turbine.ReceiveTurbine +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.LocationService +import io.element.android.features.location.test.FakeLocationService +import io.element.android.features.messages.impl.FakeMessagesNavigator +import io.element.android.features.messages.impl.MessagesNavigator +import io.element.android.features.messages.impl.draft.ComposerDraftService +import io.element.android.features.messages.impl.draft.FakeComposerDraftService +import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor +import io.element.android.features.messages.impl.timeline.TimelineController +import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter +import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper +import io.element.android.features.messages.impl.utils.TextPillificationHelper +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_USER_ID +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.room.FakeJoinedRoom +import io.element.android.libraries.mediapickers.api.PickerProvider +import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaupload.api.MediaOptimizationConfig +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaSenderFactory +import io.element.android.libraries.mediaupload.impl.DefaultMediaSender +import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory +import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.preferences.api.store.VideoCompressionPreset +import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore +import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.test.FakeSlashCommandService +import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme +import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class MessageComposerPresenterSlashCommandTest { + @get:Rule + val warmUpRule = WarmUpRule() + + private val pickerProvider = FakePickerProvider().apply { + givenResult(mockk()) // Uri is not available in JVM, so the only way to have a non-null Uri is using Mockk + } + private val mediaPreProcessor = FakeMediaPreProcessor() + private val snackbarDispatcher = SnackbarDispatcher() + private val mockMediaUrl: Uri = mockk("localMediaUri") + private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl) + private val analyticsService = FakeAnalyticsService() + private val notificationConversationService = FakeNotificationConversationService() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitFirstItem() + assertThat(initialState.isFullScreen).isFalse() + assertThat(initialState.textEditorState.messageHtml()).isEmpty() + assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal) + assertThat(initialState.showAttachmentSourcePicker).isFalse() + assertThat(initialState.canShareLocation).isTrue() + } + } + + @Test + fun `present - slash command error sets failure`() = runTest { + val presenter = createPresenter( + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.ErrorUnknownSlashCommand(A_FAILURE_REASON) } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + val errorState = awaitItem() + assertThat(errorState.slashCommandAction.isFailure()).isTrue() + assertThat(errorState.slashCommandAction.errorOrNull()?.message).isEqualTo(A_FAILURE_REASON) + // Composer should not be reset when command is an error + assertThat(errorState.textEditorState.messageHtml()).isEqualTo(A_MESSAGE) + // Close the error + errorState.eventSink(MessageComposerEvent.ClearSlashError) + val finalState = awaitItem() + assertThat(finalState.slashCommandAction.isUninitialized()).isTrue() + } + } + + @Test + fun `present - slash command navigation ShowUser navigates to member and resets composer`() = runTest { + val navigateToMember = lambdaRecorder {} + val navigator = FakeMessagesNavigator(navigateToMemberLambda = navigateToMember) + val presenter = createPresenter( + navigator = navigator, + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.ShowUser(A_USER_ID) } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + advanceUntilIdle() + // navigation should be invoked and composer reset + navigateToMember.assertions().isCalledOnce().with(value(A_USER_ID)) + assertThat(initialState.textEditorState.messageHtml()).isEmpty() + } + } + + @Test + fun `present - slash command navigation DevTools navigates to developer settings and resets composer`() = runTest { + val navigateToDev = lambdaRecorder { } + val navigator = FakeMessagesNavigator(navigateToDeveloperSettingsLambda = navigateToDev) + val presenter = createPresenter( + navigator = navigator, + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.DevTools } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + advanceUntilIdle() + navigateToDev.assertions().isCalledOnce() + assertThat(initialState.textEditorState.messageHtml()).isEmpty() + } + } + + @Test + fun `present - slash command send message proceeds and resets composer`() = runTest { + val presenter = createPresenter( + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.SendPlainText(A_MESSAGE) }, + proceedSendMessageResult = { _, _ -> Result.success(Unit) } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + advanceUntilIdle() + // Composer reset after successful slash send + assertThat(initialState.textEditorState.messageHtml()).isEmpty() + // Ensure no failure + assertThat(initialState.slashCommandAction.isFailure()).isFalse() + } + } + + @Test + fun `present - slash command send message failure sets failure state`() = runTest { + val presenter = createPresenter( + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.SendPlainText("A_MESSAGE") }, + proceedSendMessageResult = { _, _ -> Result.failure(Exception(A_FAILURE_REASON)) } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + val failureState = awaitItem() + assertThat(failureState.slashCommandAction.isFailure()).isTrue() + assertThat(failureState.slashCommandAction.errorOrNull()?.message).isEqualTo(A_FAILURE_REASON) + // Clear the error + failureState.eventSink(MessageComposerEvent.ClearSlashError) + val finalState = awaitItem() + assertThat(finalState.slashCommandAction.isUninitialized()).isTrue() + } + } + + @Test + fun `present - slash command admin proceeds and resets state on success`() = runTest { + val presenter = createPresenter( + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.BanUser(A_USER_ID, null) }, + proceedAdminResult = { _ -> Result.success(Unit) } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + val loadingState = awaitItem() + assertThat(loadingState.slashCommandAction.isLoading()).isTrue() + val successState = awaitItem() + // After success, state should be Uninitialized + assertThat(successState.slashCommandAction.isUninitialized()).isTrue() + assertThat(successState.textEditorState.messageHtml()).isEmpty() + } + } + + @Test + fun `present - slash command admin proceeds and emit failure on error`() = runTest { + val presenter = createPresenter( + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.BanUser(A_USER_ID, null) }, + proceedAdminResult = { _ -> Result.failure(Exception(A_FAILURE_REASON)) } + ) + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.textEditorState.setHtml(A_MESSAGE) + initialState.eventSink(MessageComposerEvent.SendMessage) + val loadingState = awaitItem() + assertThat(loadingState.slashCommandAction.isLoading()).isTrue() + val failureState = awaitItem() + assertThat(failureState.slashCommandAction.isFailure()).isTrue() + assertThat(failureState.slashCommandAction.errorOrNull()?.message).isEqualTo(A_FAILURE_REASON) + // Clear error + failureState.eventSink(MessageComposerEvent.ClearSlashError) + val finalState = awaitItem() + assertThat(finalState.slashCommandAction.isUninitialized()).isTrue() + } + } + + private fun TestScope.createPresenter( + room: JoinedRoom = FakeJoinedRoom( + typingNoticeResult = { Result.success(Unit) } + ), + timeline: Timeline = room.liveTimeline, + navigator: MessagesNavigator = FakeMessagesNavigator(), + pickerProvider: PickerProvider = this@MessageComposerPresenterSlashCommandTest.pickerProvider, + locationService: LocationService = FakeLocationService(true), + sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), + mediaPreProcessor: MediaPreProcessor = this@MessageComposerPresenterSlashCommandTest.mediaPreProcessor, + snackbarDispatcher: SnackbarDispatcher = this@MessageComposerPresenterSlashCommandTest.snackbarDispatcher, + permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(), + permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(), + permalinkParser: PermalinkParser = FakePermalinkParser(), + mentionSpanProvider: MentionSpanProvider = MentionSpanProvider( + permalinkParser = permalinkParser, + mentionSpanFormatter = FakeMentionSpanFormatter(), + mentionSpanTheme = MentionSpanTheme(A_USER_ID) + ), + textPillificationHelper: TextPillificationHelper = FakeTextPillificationHelper(), + isRichTextEditorEnabled: Boolean = true, + draftService: ComposerDraftService = FakeComposerDraftService(), + mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), + isInThread: Boolean = false, + slashCommandService: SlashCommandService = FakeSlashCommandService(), + ) = MessageComposerPresenter( + navigator = navigator, + sessionCoroutineScope = this, + isInThread = isInThread, + room = room, + mediaPickerProvider = pickerProvider, + sessionPreferencesStore = sessionPreferencesStore, + localMediaFactory = localMediaFactory, + mediaSenderFactory = MediaSenderFactory { timelineMode -> + DefaultMediaSender( + preProcessor = mediaPreProcessor, + room = room, + timelineMode = timelineMode, + mediaOptimizationConfigProvider = { + MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.STANDARD + ) + } + ) + }, + snackbarDispatcher = snackbarDispatcher, + analyticsService = analyticsService, + locationService = locationService, + messageComposerContext = DefaultMessageComposerContext(), + richTextEditorStateFactory = TestRichTextEditorStateFactory(), + roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), + permalinkParser = permalinkParser, + permalinkBuilder = permalinkBuilder, + timelineController = TimelineController(room, timeline), + draftService = draftService, + mentionSpanProvider = mentionSpanProvider, + pillificationHelper = textPillificationHelper, + suggestionsProcessor = SuggestionsProcessor(slashCommandService = slashCommandService), + mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, + notificationConversationService = notificationConversationService, + slashCommandService = slashCommandService, + ).apply { + isTesting = true + showTextFormatting = isRichTextEditorEnabled + } + + private suspend fun ReceiveTurbine.awaitFirstItem(): T { + skipItems(1) + return awaitItem() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index e16236f109..7a2cc1110a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper import io.element.android.features.messages.impl.utils.TextPillificationHelper +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.api.core.EventId @@ -46,6 +47,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType +import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -89,6 +91,9 @@ import io.element.android.libraries.preferences.api.store.SessionPreferencesStor import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.push.test.notifications.conversations.FakeNotificationConversationService +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.test.FakeSlashCommandService import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion @@ -144,6 +149,7 @@ class MessageComposerPresenterTest { assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal) assertThat(initialState.showAttachmentSourcePicker).isFalse() assertThat(initialState.canShareLocation).isTrue() + assertThat(initialState.slashCommandAction).isEqualTo(AsyncAction.Uninitialized) } } @@ -374,10 +380,13 @@ class MessageComposerPresenterTest { val presenter = createPresenter( room = FakeJoinedRoom( liveTimeline = FakeTimeline().apply { - sendMessageLambda = { _, _, _ -> Result.success(Unit) } + sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) } }, typingNoticeResult = { Result.success(Unit) } ), + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.NotACommand } + ), ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() @@ -409,10 +418,13 @@ class MessageComposerPresenterTest { isRichTextEditorEnabled = false, room = FakeJoinedRoom( liveTimeline = FakeTimeline().apply { - sendMessageLambda = { _, _, _ -> Result.success(Unit) } + sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) } }, typingNoticeResult = { Result.success(Unit) } ), + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.NotACommand } + ), ) moleculeFlow(RecompositionMode.Immediate) { val state = presenter.present() @@ -602,7 +614,7 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List, _: Boolean -> + val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List, _: Boolean, _: MsgType -> Result.success(Unit) } val timeline = FakeTimeline().apply { @@ -633,7 +645,7 @@ class MessageComposerPresenterTest { assert(replyMessageLambda) .isCalledOnce() - .with(any(), value(A_REPLY), value(A_REPLY), any(), value(false)) + .with(any(), value(A_REPLY), value(A_REPLY), any(), value(false), value(MsgType.MSG_TYPE_TEXT)) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -967,7 +979,12 @@ class MessageComposerPresenterTest { ) givenRoomInfo(aRoomInfo(isDirect = false)) } - val presenter = createPresenter(room) + val presenter = createPresenter( + room = room, + slashCommandService = FakeSlashCommandService( + getSuggestionsResult = { _, _ -> emptyList() }, + ), + ) presenter.test { val initialState = awaitItem() @@ -1086,13 +1103,13 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List, _: Boolean -> + val replyMessageLambda = lambdaRecorder { _: EventId?, _: String, _: String?, _: List, _: Boolean, _: MsgType -> Result.success(Unit) } val editMessageLambda = lambdaRecorder { _: EventOrTransactionId, _: String, _: String?, _: List -> Result.success(Unit) } - val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List -> + val sendMessageResult = lambdaRecorder { _: String, _: String?, _: List, _: MsgType, _: Boolean -> Result.success(Unit) } val timeline = FakeTimeline().apply { @@ -1104,7 +1121,12 @@ class MessageComposerPresenterTest { liveTimeline = timeline, typingNoticeResult = { Result.success(Unit) } ) - val presenter = createPresenter(room = room) + val presenter = createPresenter( + room = room, + slashCommandService = FakeSlashCommandService( + parseResult = { _, _, _ -> SlashCommand.NotACommand } + ), + ) presenter.test { val initialState = awaitFirstItem() @@ -1122,7 +1144,7 @@ class MessageComposerPresenterTest { advanceUntilIdle() sendMessageResult.assertions().isCalledOnce() - .with(value(A_MESSAGE), any(), value(listOf(IntentionalMention.User(A_USER_ID)))) + .with(value(A_MESSAGE), any(), value(listOf(IntentionalMention.User(A_USER_ID))), value(MsgType.MSG_TYPE_TEXT), value(false)) // Check intentional mentions on reply sent initialState.eventSink(MessageComposerEvent.SetMode(aReplyMode())) @@ -1139,7 +1161,7 @@ class MessageComposerPresenterTest { assert(replyMessageLambda) .isCalledOnce() - .with(any(), any(), any(), value(listOf(IntentionalMention.User(A_USER_ID_2))), value(false)) + .with(any(), any(), any(), value(listOf(IntentionalMention.User(A_USER_ID_2))), value(false), value(MsgType.MSG_TYPE_TEXT)) // Check intentional mentions on edit message skipItems(1) @@ -1512,9 +1534,12 @@ class MessageComposerPresenterTest { isRichTextEditorEnabled: Boolean = true, draftService: ComposerDraftService = FakeComposerDraftService(), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), + isInThread: Boolean = false, + slashCommandService: SlashCommandService = FakeSlashCommandService(), ) = MessageComposerPresenter( navigator = navigator, sessionCoroutineScope = this, + isInThread = isInThread, room = room, mediaPickerProvider = pickerProvider, sessionPreferencesStore = sessionPreferencesStore, @@ -1545,9 +1570,10 @@ class MessageComposerPresenterTest { draftService = draftService, mentionSpanProvider = mentionSpanProvider, pillificationHelper = textPillificationHelper, - suggestionsProcessor = SuggestionsProcessor(), + suggestionsProcessor = SuggestionsProcessor(slashCommandService = slashCommandService), mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, notificationConversationService = notificationConversationService, + slashCommandService = slashCommandService, ).apply { isTesting = true showTextFormatting = isRichTextEditorEnabled diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt index daba41fb3c..6283d7236a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsProcessorTest.kt @@ -17,6 +17,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomSummary +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion +import io.element.android.libraries.slashcommands.test.FakeSlashCommandService import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType @@ -27,10 +29,13 @@ import org.junit.Test class SuggestionsProcessorTest { private fun aMentionSuggestion(text: String) = Suggestion(0, 1, SuggestionType.Mention, text) private fun aRoomSuggestion(text: String) = Suggestion(0, 1, SuggestionType.Room, text) - private val aCommandSuggestion = Suggestion(0, 1, SuggestionType.Command, "") private val aCustomSuggestion = Suggestion(0, 1, SuggestionType.Custom("*"), "") - private val suggestionsProcessor = SuggestionsProcessor() + private val suggestionsProcessor = SuggestionsProcessor( + slashCommandService = FakeSlashCommandService( + getSuggestionsResult = { _, _ -> emptyList() }, + ), + ) @Test fun `processing null suggestion will return empty suggestion`() = runTest { @@ -40,18 +45,59 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @Test - fun `processing Command will return empty suggestion`() = runTest { - val result = suggestionsProcessor.process( - suggestion = aCommandSuggestion, + fun `processing Command will return suggestions from the slash service`() = runTest { + val suggestionsProcessorWithCommand = SuggestionsProcessor( + slashCommandService = FakeSlashCommandService( + getSuggestionsResult = { _, _ -> + listOf( + SlashCommandSuggestion( + command = "aCommand", + parameters = null, + description = "A description", + ), + ) + }, + ), + ) + val result = suggestionsProcessorWithCommand.process( + suggestion = Suggestion(0, 1, SuggestionType.Command, ""), roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())), roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, + ) + assertThat(result).isNotEmpty() + } + + @Test + fun `processing Command will return empty list if start of suggestion is not 0`() = runTest { + val suggestionsProcessorWithCommand = SuggestionsProcessor( + slashCommandService = FakeSlashCommandService( + getSuggestionsResult = { _, _ -> + listOf( + SlashCommandSuggestion( + command = "aCommand", + parameters = null, + description = "A description", + ), + ) + }, + ), + ) + val result = suggestionsProcessorWithCommand.process( + suggestion = Suggestion(1, 2, SuggestionType.Command, ""), + roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember())), + roomAliasSuggestions = emptyList(), + currentUserId = A_USER_ID, + canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -64,6 +110,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -76,6 +123,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -88,6 +136,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -100,6 +149,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -120,6 +170,7 @@ class SuggestionsProcessorTest { ), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( @@ -149,6 +200,7 @@ class SuggestionsProcessorTest { ), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( @@ -178,6 +230,7 @@ class SuggestionsProcessorTest { ), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -198,6 +251,7 @@ class SuggestionsProcessorTest { ), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( @@ -227,6 +281,7 @@ class SuggestionsProcessorTest { ), currentUserId = A_USER_ID, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -240,6 +295,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( @@ -257,6 +313,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = UserId("@alice:server.org"), canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -270,6 +327,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -283,6 +341,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEmpty() } @@ -296,6 +355,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( @@ -313,6 +373,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { true }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( @@ -331,6 +392,7 @@ class SuggestionsProcessorTest { roomAliasSuggestions = emptyList(), currentUserId = A_USER_ID_2, canSendRoomMention = { false }, + isInThread = false, ) assertThat(result).isEqualTo( listOf( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt index 479139a45b..69eaae3699 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/FakePinnedMessagesListNavigator.kt @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator { @@ -26,4 +27,9 @@ class FakePinnedMessagesListNavigator : PinnedMessagesListNavigator { override fun forwardEvent(eventId: EventId) { onForwardEventClickLambda?.invoke(eventId) } + + var onOpenThreadLambda: ((ThreadId) -> Unit)? = null + override fun navigateToThread(threadRootId: ThreadId) { + onOpenThreadLambda?.invoke(threadRootId) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt new file mode 100644 index 0000000000..9f8d210a16 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.threads + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.fixtures.aTimelineItemContentFactory +import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter +import io.element.android.features.messages.impl.threads.list.ThreadsListEvents +import io.element.android.features.messages.impl.threads.list.ThreadsListPresenter +import io.element.android.features.messages.impl.threads.list.aThreadListItem +import io.element.android.libraries.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ThreadsListPresenterTest { + @Test + fun `present - initial state`() = runTest { + createThreadsListPresenter().test { + awaitItem().run { + assertThat(threads).isEmpty() + assertThat(roomId).isEqualTo(A_ROOM_ID) + assertThat(roomName).isEqualTo(A_ROOM_NAME) + assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL) + } + } + } + + @Test + fun `present - paginate`() = runTest { + val paginateRecorder = lambdaRecorder> { Result.success(Unit) } + val threadsListService = FakeThreadsListService(paginate = paginateRecorder) + val room = FakeJoinedRoom(threadsListService = threadsListService) + createThreadsListPresenter(room).test { + val initialItem = awaitItem() + + // Pagination is automatically triggered on start, so we should have one call to paginate already + paginateRecorder.assertions().isCalledOnce() + + initialItem.eventSink(ThreadsListEvents.Paginate) + + // Simulate a pagination result + threadsListService.emit(listOf(aThreadListItem())) + + // We should have a second call to paginate after the event is sent + paginateRecorder.assertions().isCalledExactly(2) + + // And we receive the new items + assertThat(awaitItem().threads).isNotEmpty() + } + } + + private fun createThreadsListPresenter( + room: FakeJoinedRoom = FakeJoinedRoom(), + ): ThreadsListPresenter { + return ThreadsListPresenter( + room = room, + timelineItemContentFactory = aTimelineItemContentFactory(), + messageSummaryFormatter = FakeMessageSummaryFormatter(), + dateFormatter = FakeDateFormatter(), + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt index 034c952f3d..bc36766bac 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt @@ -12,6 +12,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID @@ -154,10 +155,10 @@ class TimelineControllerTest { @Test fun `test invokeOnCurrentTimeline use the detached timeline and not the live timeline`() = runTest { - val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List -> + val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List, _: MsgType, _: Boolean -> Result.success(Unit) } - val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List -> + val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List, _: MsgType, _: Boolean -> Result.success(Unit) } val liveTimeline = FakeTimeline(name = "live").apply { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 3a97fbd9dc..c05625e2d3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -67,24 +67,31 @@ class TimelineViewTest { @Test fun `reaching the end of the timeline does not send a LoadMore event`() { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), eventSink = eventsRecorder, ), ) + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) } @Test fun `scroll to bottom on live timeline does not emit the Event`() { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = true, eventSink = eventsRecorder, ), forceJumpToBottomVisibility = true, ) + + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) + eventsRecorder.clear() + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) rule.onNodeWithContentDescription(contentDescription).performClick() } @@ -94,15 +101,33 @@ class TimelineViewTest { val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, eventSink = eventsRecorder, ), ) + + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) + eventsRecorder.clear() + val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertSingle(TimelineEvent.JumpToLive) } + @Test + fun `an empty timeline triggers a prefetch`() { + val eventsRecorder = EventsRecorder() + rule.setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf(), + eventSink = eventsRecorder, + ), + ) + + eventsRecorder.assertSingle(TimelineEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + } + @Test fun `show shield dialog`() { val eventsRecorder = EventsRecorder() @@ -133,11 +158,15 @@ class TimelineViewTest { val eventsRecorder = EventsRecorder() rule.setTimelineView( state = aTimelineState( + timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, eventSink = eventsRecorder, messageShield = aCriticalShield(), ), ) + eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) + eventsRecorder.clear() + rule.clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) } diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts index 29c00ece73..b839f8de06 100644 --- a/features/messages/test/build.gradle.kts +++ b/features/messages/test/build.gradle.kts @@ -26,5 +26,5 @@ dependencies { implementation(projects.libraries.voicerecorder.test) implementation(projects.services.analytics.test) implementation(projects.tests.testutils) - implementation(projects.libraries.mediaupload.impl) + implementation(projects.libraries.mediaupload.api) } diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt index 17de179b55..466bfba4fd 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/timeline/voicemessages/composer/FakeDefaultVoiceMessageComposerPresenterFactory.kt @@ -12,13 +12,10 @@ import io.element.android.features.messages.impl.voicemessages.composer.DefaultV import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.mediaplayer.test.FakeAudioFocus import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender -import io.element.android.libraries.mediaupload.impl.DefaultMediaSender -import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfigProvider -import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.mediaupload.test.FakeMediaSender import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService @@ -26,12 +23,7 @@ import kotlinx.coroutines.CoroutineScope class FakeDefaultVoiceMessageComposerPresenterFactory( private val sessionCoroutineScope: CoroutineScope, - private val mediaSender: MediaSender = DefaultMediaSender( - preProcessor = FakeMediaPreProcessor(), - room = FakeJoinedRoom(), - timelineMode = Timeline.Mode.Live, - mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), - ), + private val mediaSender: MediaSender = FakeMediaSender(), ) : DefaultVoiceMessageComposerPresenter.Factory { override fun create(timelineMode: Timeline.Mode): DefaultVoiceMessageComposerPresenter { return DefaultVoiceMessageComposerPresenter( diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts index a37c3be882..1ba0953349 100644 --- a/features/migration/impl/build.gradle.kts +++ b/features/migration/impl/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation(projects.features.migration.api) implementation(projects.libraries.architecture) implementation(projects.libraries.androidutils) - implementation(projects.libraries.preferences.impl) + implementation(projects.libraries.preferences.api) implementation(libs.androidx.datastore.preferences) implementation(projects.features.rageshake.api) implementation(projects.libraries.designsystem) diff --git a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt index 7cffa057bc..6981d2c6af 100644 --- a/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt +++ b/features/networkmonitor/impl/src/main/kotlin/io/element/android/features/networkmonitor/impl/DefaultNetworkMonitor.kt @@ -15,13 +15,11 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import android.os.Build import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus -import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext import kotlinx.coroutines.CoroutineScope @@ -44,7 +42,6 @@ import java.util.concurrent.atomic.AtomicInteger class DefaultNetworkMonitor( @ApplicationContext context: Context, @AppCoroutineScope appCoroutineScope: CoroutineScope, - private val buildMeta: BuildMeta, ) : NetworkMonitor { private val connectivityManager: ConnectivityManager = context.getSystemService(ConnectivityManager::class.java) @@ -76,17 +73,10 @@ class DefaultNetworkMonitor( } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - if (!buildMeta.isEnterpriseBuild) { - // The air-gapped environment detection is only relevant for the enterprise build. - return - } - if (network.networkHandle == connectivityManager.activeNetwork?.networkHandle) { // If the network doesn't have the NET_CAPABILITY_VALIDATED capability, it means that the network is not able to reach the internet // (according to Google), which is a common case in air-gapped environments. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - isInAirGappedEnvironment.value = !networkCapabilities.capabilities.contains(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - } + isInAirGappedEnvironment.value = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } } diff --git a/features/poll/api/src/main/res/values-ja/translations.xml b/features/poll/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c66e27d85d --- /dev/null +++ b/features/poll/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + + "総投票数うち%1$d%" + + "前の項目を削除します" + "投票の勝者はこちらです" + diff --git a/features/poll/api/src/main/res/values-vi/translations.xml b/features/poll/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..21651828df --- /dev/null +++ b/features/poll/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Xóa lựa chọn trước đó" + "Đây là câu trả lời chiến thắng" + diff --git a/features/poll/impl/src/main/res/values-ja/translations.xml b/features/poll/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..a4cd3fbff3 --- /dev/null +++ b/features/poll/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,20 @@ + + + "選択肢を追加" + "結果を投票終了後に表示します" + "投票を非表示" + "選択肢 %1$d" + "変更は保存されていません。本当に戻りますか?" + "選択肢を削除 %1$s" + "質問またはトピック" + "何についての投票ですか?" + "投票を作成" + "本当にこの投票を削除しますか?" + "投票を削除" + "投票を編集" + "進行中の投票が見つかりません。" + "過去の投票が見つかりません。" + "進行中" + "過去" + "投票" + diff --git a/features/poll/impl/src/main/res/values-vi/translations.xml b/features/poll/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..dc29666c74 --- /dev/null +++ b/features/poll/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,19 @@ + + + "Thêm lựa chọn" + "Chỉ hiển thị kết quả sau khi cuộc thăm dò kết thúc" + "Ẩn phiếu" + "Lựa chọn %1$d" + "Các thay đổi của bạn chưa được lưu. Bạn có chắc muốn quay lại không?" + "Câu hỏi hoặc chủ đề" + "Cuộc thăm dò này về vấn đề gì?" + "Tạo Cuộc thăm dò ý kiến" + "Bạn có chắc chắn muốn xóa cuộc thăm dò này không?" + "Xóa cuộc thăm dò" + "Sửa cuộc thăm dò" + "Không tìm thấy bất kỳ cuộc thăm dò nào đang diễn ra." + "Không tìm thấy bất kỳ cuộc thăm dò nào trước đây." + "Đang diễn ra" + "Quá khứ" + "Cuộc thăm dò ý kiến" + diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt index 5a59d9be8a..e7fbe6069f 100644 --- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt @@ -28,6 +28,9 @@ interface PreferencesEntryPoint : FeatureEntryPoint { @Parcelize data object NotificationTroubleshoot : InitialTarget + + @Parcelize + data object DeveloperSettings : InitialTarget } data class Params(val initialElement: InitialTarget) : NodeInputs @@ -47,4 +50,14 @@ interface PreferencesEntryPoint : FeatureEntryPoint { fun navigateToRoomNotificationSettings(roomId: RoomId) fun navigateToEvent(roomId: RoomId, eventId: EventId) } + + fun createAppDeveloperSettingsNode( + parentNode: Node, + buildContext: BuildContext, + callback: DeveloperSettingsCallback, + ): Node + + interface DeveloperSettingsCallback : Plugin { + fun onDone() + } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt index 4348b33756..bacf1bfb48 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsNode import io.element.android.libraries.architecture.createNode @ContributesBinding(AppScope::class) @@ -28,10 +29,22 @@ class DefaultPreferencesEntryPoint : PreferencesEntryPoint { plugins = listOf(params, callback) ) } + + override fun createAppDeveloperSettingsNode( + parentNode: Node, + buildContext: BuildContext, + callback: PreferencesEntryPoint.DeveloperSettingsCallback, + ): Node { + return parentNode.createNode( + buildContext = buildContext, + plugins = listOf(callback), + ) + } } internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) { is PreferencesEntryPoint.InitialTarget.Root -> PreferencesFlowNode.NavTarget.Root is PreferencesEntryPoint.InitialTarget.NotificationSettings -> PreferencesFlowNode.NavTarget.NotificationSettings PreferencesEntryPoint.InitialTarget.NotificationTroubleshoot -> PreferencesFlowNode.NavTarget.TroubleshootNotifications + PreferencesEntryPoint.InitialTarget.DeveloperSettings -> PreferencesFlowNode.NavTarget.DeveloperSettings } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index c646923c77..15718ca0f0 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -192,7 +192,11 @@ class PreferencesFlowNode( } override fun onDone() { - backstack.pop() + if (backstack.canPop()) { + backstack.pop() + } else { + navigateUp() + } } } createNode(buildContext, listOf(developerSettingsCallback)) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index 1804d7e070..12de2be746 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -9,15 +9,8 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.ui.graphics.Color -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -import io.element.android.libraries.matrix.api.tracing.TraceLogPack sealed interface DeveloperSettingsEvents { - data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : DeveloperSettingsEvents - data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents - data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents - data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index a0d96be540..1598c2ef27 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -11,61 +11,37 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.toArgb import dev.zacsweers.metro.Inject import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.features.preferences.impl.developer.tracing.toLogLevel -import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem -import io.element.android.features.preferences.impl.model.EnabledFeature +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase -import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.data.ByteUnit -import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.preferences.api.store.AppPreferencesStore -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import java.net.URL @Inject class DeveloperSettingsPresenter( + private val appDeveloperSettingsPresenter: Presenter, private val sessionId: SessionId, - private val featureFlagService: FeatureFlagService, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val clearCacheUseCase: ClearCacheUseCase, - private val rageshakePresenter: Presenter, - private val appPreferencesStore: AppPreferencesStore, - private val buildMeta: BuildMeta, private val enterpriseService: EnterpriseService, private val vacuumStoresUseCase: VacuumStoresUseCase, private val databaseSizesUseCase: GetDatabaseSizesUseCase, @@ -73,10 +49,6 @@ class DeveloperSettingsPresenter( ) : Presenter { @Composable override fun present(): DeveloperSettingsState { - val rageshakeState = rageshakePresenter.present() - val enabledFeatures = remember { - mutableStateListOf() - } val cacheSize = remember { mutableStateOf>(AsyncData.Uninitialized) } @@ -89,38 +61,9 @@ class DeveloperSettingsPresenter( var showColorPicker by remember { mutableStateOf(false) } - val customElementCallBaseUrl by remember { - appPreferencesStore - .getCustomElementCallBaseUrlFlow() - }.collectAsState(initial = null) - - val tracingLogLevelFlow = remember { - appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) } - } - val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized) - val tracingLogPacks by produceState(persistentListOf()) { - appPreferencesStore.getTracingLogPacksFlow() - // Sort the entries alphabetically by its title - .map { it.sortedBy { pack -> pack.title } } - .collectLatest { value = it.toImmutableList() } - } - LaunchedEffect(Unit) { computeDatabaseSizes(databaseSizes) - featureFlagService.getAvailableFeatures() - .run { - // Never display room directory search in release builds for Play Store - if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) { - filterNot { it.key == FeatureFlags.RoomDirectorySearch.key } - } else { - this - } - } - .forEach { feature -> - enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature))) - } } - val featureUiModels = createUiModels(enabledFeatures) val coroutineScope = rememberCoroutineScope() // Compute cache size each time the clear cache action value is changed LaunchedEffect(clearCacheAction.value.isSuccess()) { @@ -129,29 +72,7 @@ class DeveloperSettingsPresenter( fun handleEvent(event: DeveloperSettingsEvents) { when (event) { - is DeveloperSettingsEvents.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature( - enabledFeatures = enabledFeatures, - featureKey = event.feature.key, - enabled = event.isEnabled, - triggerClearCache = { handleEvent(DeveloperSettingsEvents.ClearCache) } - ) - is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch { - val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() } - appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) - } DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) - is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch { - appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel()) - } - is DeveloperSettingsEvents.ToggleTracingLogPack -> coroutineScope.launch { - val currentPacks = tracingLogPacks.toMutableSet() - if (currentPacks.contains(event.logPack)) { - currentPacks.remove(event.logPack) - } else { - currentPacks.add(event.logPack) - } - appPreferencesStore.setTracingLogPacks(currentPacks) - } is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch { showColorPicker = false val color = event.color @@ -170,56 +91,18 @@ class DeveloperSettingsPresenter( } } + val appDeveloperSettingsState = appDeveloperSettingsPresenter.present() return DeveloperSettingsState( - features = featureUiModels, + appDeveloperSettingsState = appDeveloperSettingsState, cacheSize = cacheSize.value, databaseSizes = databaseSizes.value, clearCacheAction = clearCacheAction.value, - rageshakeState = rageshakeState, - customElementCallBaseUrlState = CustomElementCallBaseUrlState( - baseUrl = customElementCallBaseUrl, - validator = ::customElementCallUrlValidator, - ), - tracingLogLevel = tracingLogLevel, - tracingLogPacks = tracingLogPacks, isEnterpriseBuild = enterpriseService.isEnterpriseBuild, showColorPicker = showColorPicker, eventSink = ::handleEvent, ) } - @Composable - private fun createUiModels( - enabledFeatures: SnapshotStateList, - ): ImmutableList { - return enabledFeatures.map { enabledFeature -> - key(enabledFeature.feature.key) { - remember(enabledFeature) { - FeatureUiModel( - key = enabledFeature.feature.key, - title = enabledFeature.feature.title, - description = enabledFeature.feature.description, - icon = null, - isEnabled = enabledFeature.isEnabled - ) - } - } - }.toImmutableList() - } - - private fun CoroutineScope.updateEnabledFeature( - enabledFeatures: SnapshotStateList, - featureKey: String, - enabled: Boolean, - @Suppress("UNUSED_PARAMETER") triggerClearCache: () -> Unit, - ) = launch { - val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == featureKey }.takeIf { it != -1 } ?: return@launch - val feature = enabledFeatures[featureIndex].feature - if (featureFlagService.setFeatureEnabled(feature, enabled)) { - enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = enabled) - } - } - private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch { suspend { computeCacheSizeUseCase() @@ -253,12 +136,3 @@ class DeveloperSettingsPresenter( }.runCatchingUpdatingState(clearCacheAction) } } - -private fun customElementCallUrlValidator(url: String?): Boolean { - return runCatchingExceptions { - if (url.isNullOrEmpty()) return@runCatchingExceptions - val parsedUrl = URL(url) - if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") - if (parsedUrl.host.isNullOrBlank()) error("Missing host") - }.isSuccess -} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 920c8ec95c..fa5859a028 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -8,32 +8,19 @@ package io.element.android.features.preferences.impl.developer -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -import io.element.android.libraries.matrix.api.tracing.TraceLogPack -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap data class DeveloperSettingsState( - val features: ImmutableList, + val appDeveloperSettingsState: AppDeveloperSettingsState, val cacheSize: AsyncData, val databaseSizes: AsyncData>, - val rageshakeState: RageshakePreferencesState, val clearCacheAction: AsyncAction, - val customElementCallBaseUrlState: CustomElementCallBaseUrlState, - val tracingLogLevel: AsyncData, - val tracingLogPacks: ImmutableList, val isEnterpriseBuild: Boolean, val showColorPicker: Boolean, val eventSink: (DeveloperSettingsEvents) -> Unit ) { val showLoader = clearCacheAction is AsyncAction.Loading } - -data class CustomElementCallBaseUrlState( - val baseUrl: String?, - val validator: (String?) -> Boolean, -) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index b925eabe9e..28aefd3ad1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -9,14 +9,11 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState +import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList -import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toImmutableList open class DeveloperSettingsStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,11 +22,6 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, - customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), - traceLogPacks: List = emptyList(), isEnterpriseBuild: Boolean = false, showColorPicker: Boolean = false, eventSink: (DeveloperSettingsEvents) -> Unit = {}, ) = DeveloperSettingsState( - features = aFeatureUiModelList(), - rageshakeState = aRageshakePreferencesState(), + appDeveloperSettingsState = appDeveloperSettingsState, cacheSize = AsyncData.Success("1.2 MB"), databaseSizes = AsyncData.Success(persistentMapOf("state_store" to "1.2MB")), clearCacheAction = clearCacheAction, - customElementCallBaseUrlState = customElementCallBaseUrlState, - tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), - tracingLogPacks = traceLogPacks.toImmutableList(), isEnterpriseBuild = isEnterpriseBuild, showColorPicker = showColorPicker, eventSink = eventSink, ) - -fun aCustomElementCallBaseUrlState( - baseUrl: String? = null, - validator: (String?) -> Boolean = { true }, -) = CustomElementCallBaseUrlState( - baseUrl = baseUrl, - validator = validator, -) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 444a391d43..3adf9a13de 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -10,40 +10,28 @@ package io.element.android.features.preferences.impl.developer import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme import io.element.android.features.preferences.impl.R -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory -import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown import io.element.android.libraries.designsystem.components.preferences.PreferencePage -import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField 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.Text -import io.element.android.libraries.featureflag.ui.FeatureListView -import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.ui.strings.CommonStrings import io.mhssn.colorpicker.ColorPickerDialog import io.mhssn.colorpicker.ColorPickerType -import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -71,52 +59,12 @@ fun DeveloperSettingsView( title = stringResource(id = CommonStrings.common_developer_options) ) { // Note: this is OK to hardcode strings in this debug screen. - PreferenceCategory( - title = "Feature flags", - ) { - FeatureListContent(state) - } - NotificationCategory(onPushHistoryClick) - ElementCallCategory(state = state) - - PreferenceCategory(title = "Rust SDK") { - PreferenceDropdown( - title = "Tracing log level", - supportingText = "Requires app reboot", - selectedOption = state.tracingLogLevel.dataOrNull(), - options = LogLevelItem.entries.toImmutableList(), - onSelectOption = { logLevel -> - state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel)) - } - ) - } - PreferenceCategory(title = "Enable trace logs per SDK feature") { - Text( - text = "Requires app reboot", - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - ) - for (logPack in TraceLogPack.entries) { - PreferenceSwitch( - title = logPack.title, - isChecked = state.tracingLogPacks.contains(logPack), - onCheckedChange = { isChecked -> state.eventSink(DeveloperSettingsEvents.ToggleTracingLogPack(logPack, isChecked)) } - ) - } - } - - PreferenceCategory(title = "Showkase") { - ListItem( - headlineContent = { - Text("Open Showkase browser") - }, - onClick = onOpenShowkase - ) - } - RageshakePreferencesView( - state = state.rageshakeState, + AppDeveloperSettingsView( + state = state.appDeveloperSettingsState, + onOpenShowkase = onOpenShowkase, ) + NotificationCategory(onPushHistoryClick) + if (state.isEnterpriseBuild) { PreferenceCategory(title = "Theme") { ListItem( @@ -137,14 +85,6 @@ fun DeveloperSettingsView( ) } } - PreferenceCategory(title = "Crash") { - ListItem( - headlineContent = { - Text("Crash the app 💥") - }, - onClick = { error("This crash is a test.") } - ) - } val cache = state.cacheSize PreferenceCategory(title = "Cache") { ListItem( @@ -212,32 +152,6 @@ fun DeveloperSettingsView( ) } -@Composable -private fun ElementCallCategory( - state: DeveloperSettingsState, -) { - PreferenceCategory(title = "Element Call") { - val callUrlState = state.customElementCallBaseUrlState - - val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { - stringResource(R.string.screen_advanced_settings_element_call_base_url_description) - } else { - callUrlState.baseUrl - } - PreferenceTextField( - headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), - value = callUrlState.baseUrl, - placeholder = "https://.../room", - supportingText = supportingText, - validation = callUrlState.validator, - onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), - displayValue = { value -> !value.isNullOrEmpty() }, - keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri), - onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) } - ) - } -} - @Composable private fun NotificationCategory(onPushHistoryClick: () -> Unit) { PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_title)) { @@ -250,20 +164,6 @@ private fun NotificationCategory(onPushHistoryClick: () -> Unit) { } } -@Composable -private fun FeatureListContent( - state: DeveloperSettingsState, -) { - fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) { - state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled)) - } - - FeatureListView( - features = state.features, - onCheckedChange = ::onFeatureEnabled, - ) -} - @PreviewsDayNight @Composable internal fun DeveloperSettingsViewPreview( @@ -273,6 +173,6 @@ internal fun DeveloperSettingsViewPreview( state = state, onOpenShowkase = {}, onPushHistoryClick = {}, - onBackClick = {} + onBackClick = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt new file mode 100644 index 0000000000..d9641a2810 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsEvent.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack + +sealed interface AppDeveloperSettingsEvent { + data class UpdateEnabledFeature(val feature: FeatureUiModel, val isEnabled: Boolean) : AppDeveloperSettingsEvent + data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AppDeveloperSettingsEvent + data class SetTracingLogLevel(val logLevel: LogLevelItem) : AppDeveloperSettingsEvent + data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : AppDeveloperSettingsEvent +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsNode.kt new file mode 100644 index 0000000000..ae5e710d4b --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsNode.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.airbnb.android.showkase.models.Showkase +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.designsystem.showkase.getBrowserIntent + +@ContributesNode(AppScope::class) +@AssistedInject +class AppDeveloperSettingsNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: AppDeveloperSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + private val callback: PreferencesEntryPoint.DeveloperSettingsCallback = callback() + + @Composable + override fun View(modifier: Modifier) { + val activity = requireNotNull(LocalActivity.current) + fun openShowkase() { + val intent = Showkase.getBrowserIntent(activity) + activity.startActivity(intent) + } + + val state = presenter.present() + AppDeveloperSettingsPage( + state = state, + modifier = modifier, + onOpenShowkase = ::openShowkase, + onBackClick = callback::onDone, + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPage.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPage.kt new file mode 100644 index 0000000000..81e1304e7b --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPage.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun AppDeveloperSettingsPage( + state: AppDeveloperSettingsState, + onOpenShowkase: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler( + onBack = onBackClick, + ) + PreferencePage( + modifier = modifier, + onBackClick = { + onBackClick() + }, + title = "Application developer options", + ) { + AppDeveloperSettingsView( + state = state, + onOpenShowkase = onOpenShowkase, + modifier = Modifier.padding(top = 8.dp) + ) + } +} + +@PreviewsDayNight +@Composable +internal fun AppDeveloperSettingsPagePreview() = ElementPreview { + AppDeveloperSettingsPage( + state = anAppDeveloperSettingsState(), + onOpenShowkase = {}, + onBackClick = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt new file mode 100644 index 0000000000..4c76c6ec7e --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenter.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.SnapshotStateList +import dev.zacsweers.metro.Inject +import io.element.android.features.preferences.impl.developer.tracing.toLogLevel +import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem +import io.element.android.features.preferences.impl.model.EnabledFeature +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import java.net.URL + +@Inject +class AppDeveloperSettingsPresenter( + private val featureFlagService: FeatureFlagService, + private val rageshakePresenter: Presenter, + private val appPreferencesStore: AppPreferencesStore, + private val buildMeta: BuildMeta, +) : Presenter { + @Composable + override fun present(): AppDeveloperSettingsState { + val rageshakeState = rageshakePresenter.present() + val enabledFeatures = remember { + mutableStateListOf() + } + val customElementCallBaseUrl by remember { + appPreferencesStore + .getCustomElementCallBaseUrlFlow() + }.collectAsState(initial = null) + + val tracingLogLevelFlow = remember { + appPreferencesStore.getTracingLogLevelFlow().map { AsyncData.Success(it.toLogLevelItem()) } + } + val tracingLogLevel by tracingLogLevelFlow.collectAsState(initial = AsyncData.Uninitialized) + val tracingLogPacks by produceState(persistentListOf()) { + appPreferencesStore.getTracingLogPacksFlow() + // Sort the entries alphabetically by its title + .map { it.sortedBy { pack -> pack.title } } + .collectLatest { value = it.toImmutableList() } + } + + LaunchedEffect(Unit) { + featureFlagService.getAvailableFeatures() + .run { + // Never display room directory search in release builds for Play Store + if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) { + filterNot { it.key == FeatureFlags.RoomDirectorySearch.key } + } else { + this + } + } + .forEach { feature -> + enabledFeatures.add(EnabledFeature(feature, featureFlagService.isFeatureEnabled(feature))) + } + } + val featureUiModels = createUiModels(enabledFeatures) + val coroutineScope = rememberCoroutineScope() + // Compute cache size each time the clear cache action value is changed + + fun handleEvent(event: AppDeveloperSettingsEvent) { + when (event) { + is AppDeveloperSettingsEvent.UpdateEnabledFeature -> coroutineScope.updateEnabledFeature( + enabledFeatures = enabledFeatures, + featureKey = event.feature.key, + enabled = event.isEnabled, + ) + is AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl -> coroutineScope.launch { + val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() } + appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) + } + is AppDeveloperSettingsEvent.SetTracingLogLevel -> coroutineScope.launch { + appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel()) + } + is AppDeveloperSettingsEvent.ToggleTracingLogPack -> coroutineScope.launch { + val currentPacks = tracingLogPacks.toMutableSet() + if (currentPacks.contains(event.logPack)) { + currentPacks.remove(event.logPack) + } else { + currentPacks.add(event.logPack) + } + appPreferencesStore.setTracingLogPacks(currentPacks) + } + } + } + + return AppDeveloperSettingsState( + features = featureUiModels, + rageshakeState = rageshakeState, + customElementCallBaseUrlState = CustomElementCallBaseUrlState( + baseUrl = customElementCallBaseUrl, + validator = ::customElementCallUrlValidator, + ), + tracingLogLevel = tracingLogLevel, + tracingLogPacks = tracingLogPacks, + eventSink = ::handleEvent, + ) + } + + @Composable + private fun createUiModels( + enabledFeatures: SnapshotStateList, + ): ImmutableList { + return enabledFeatures.map { enabledFeature -> + key(enabledFeature.feature.key) { + remember(enabledFeature) { + FeatureUiModel( + key = enabledFeature.feature.key, + title = enabledFeature.feature.title, + description = enabledFeature.feature.description, + icon = null, + isEnabled = enabledFeature.isEnabled + ) + } + } + }.toImmutableList() + } + + private fun CoroutineScope.updateEnabledFeature( + enabledFeatures: SnapshotStateList, + featureKey: String, + enabled: Boolean, + ) = launch { + val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == featureKey }.takeIf { it != -1 } ?: return@launch + val feature = enabledFeatures[featureIndex].feature + if (featureFlagService.setFeatureEnabled(feature, enabled)) { + enabledFeatures[featureIndex] = enabledFeatures[featureIndex].copy(isEnabled = enabled) + } + } +} + +private fun customElementCallUrlValidator(url: String?): Boolean { + return runCatchingExceptions { + if (url.isNullOrEmpty()) return@runCatchingExceptions + val parsedUrl = URL(url) + if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") + if (parsedUrl.host.isNullOrBlank()) error("Missing host") + }.isSuccess +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsState.kt new file mode 100644 index 0000000000..1eb5fd7fd3 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.ImmutableList + +data class AppDeveloperSettingsState( + val features: ImmutableList, + val rageshakeState: RageshakePreferencesState, + val customElementCallBaseUrlState: CustomElementCallBaseUrlState, + val tracingLogLevel: AsyncData, + val tracingLogPacks: ImmutableList, + val eventSink: (AppDeveloperSettingsEvent) -> Unit +) + +data class CustomElementCallBaseUrlState( + val baseUrl: String?, + val validator: (String?) -> Boolean, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt new file mode 100644 index 0000000000..494b3b6bbd --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.toImmutableList + +open class AppDeveloperSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAppDeveloperSettingsState(), + anAppDeveloperSettingsState( + customElementCallBaseUrlState = aCustomElementCallBaseUrlState( + baseUrl = "https://call.element.ahoy", + ) + ), + ) +} + +fun anAppDeveloperSettingsState( + customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), + traceLogPacks: List = emptyList(), + eventSink: (AppDeveloperSettingsEvent) -> Unit = {}, +) = AppDeveloperSettingsState( + features = aFeatureUiModelList(), + rageshakeState = aRageshakePreferencesState(), + customElementCallBaseUrlState = customElementCallBaseUrlState, + tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), + tracingLogPacks = traceLogPacks.toImmutableList(), + eventSink = eventSink, +) + +fun aCustomElementCallBaseUrlState( + baseUrl: String? = null, + validator: (String?) -> Boolean = { true }, +) = CustomElementCallBaseUrlState( + baseUrl = baseUrl, + validator = validator, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt new file mode 100644 index 0000000000..71051cf829 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.preferences.impl.R +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField +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.Text +import io.element.android.libraries.featureflag.ui.FeatureListView +import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.tracing.TraceLogPack +import kotlinx.collections.immutable.toImmutableList + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun AppDeveloperSettingsView( + state: AppDeveloperSettingsState, + onOpenShowkase: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + ) { + // Note: this is OK to hardcode strings in this debug screen. + PreferenceCategory( + title = "Feature flags", + showTopDivider = false, + ) { + FeatureListContent(state) + } + ElementCallCategory(state = state) + PreferenceCategory(title = "Rust SDK") { + PreferenceDropdown( + title = "Tracing log level", + supportingText = "Requires app reboot", + selectedOption = state.tracingLogLevel.dataOrNull(), + options = LogLevelItem.entries.toImmutableList(), + onSelectOption = { logLevel -> + state.eventSink(AppDeveloperSettingsEvent.SetTracingLogLevel(logLevel)) + } + ) + } + PreferenceCategory(title = "Enable trace logs per SDK feature") { + Text( + text = "Requires app reboot", + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) + for (logPack in TraceLogPack.entries) { + PreferenceSwitch( + title = logPack.title, + isChecked = state.tracingLogPacks.contains(logPack), + onCheckedChange = { isChecked -> state.eventSink(AppDeveloperSettingsEvent.ToggleTracingLogPack(logPack, isChecked)) } + ) + } + } + PreferenceCategory(title = "Showkase") { + ListItem( + headlineContent = { + Text("Open Showkase browser") + }, + onClick = onOpenShowkase + ) + } + PreferenceCategory(title = "Crash") { + ListItem( + headlineContent = { + Text("Crash the app 💥") + }, + onClick = { error("This crash is a test.") } + ) + } + RageshakePreferencesView( + state = state.rageshakeState, + ) + PreferenceCategory(title = "Crash") { + ListItem( + headlineContent = { + Text("Crash the app 💥") + }, + onClick = { error("This crash is a test.") } + ) + } + } +} + +@Composable +private fun ElementCallCategory( + state: AppDeveloperSettingsState, +) { + PreferenceCategory(title = "Element Call") { + val callUrlState = state.customElementCallBaseUrlState + + val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { + stringResource(R.string.screen_advanced_settings_element_call_base_url_description) + } else { + callUrlState.baseUrl + } + PreferenceTextField( + headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), + value = callUrlState.baseUrl, + placeholder = "https://.../room", + supportingText = supportingText, + validation = callUrlState.validator, + onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), + displayValue = { value -> !value.isNullOrEmpty() }, + keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri), + onChange = { state.eventSink(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl(it)) } + ) + } +} + +@Composable +private fun FeatureListContent( + state: AppDeveloperSettingsState, +) { + fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) { + state.eventSink(AppDeveloperSettingsEvent.UpdateEnabledFeature(feature, isEnabled)) + } + + FeatureListView( + features = state.features, + onCheckedChange = ::onFeatureEnabled, + ) +} + +@PreviewsDayNight +@Composable +internal fun AppDeveloperSettingsViewPreview( + @PreviewParameter(AppDeveloperSettingsStateProvider::class) state: AppDeveloperSettingsState +) = ElementPreview { + AppDeveloperSettingsView( + state = state, + onOpenShowkase = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt new file mode 100644 index 0000000000..bad0ccae0f --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsPresenter +import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState +import io.element.android.libraries.architecture.Presenter + +@ContributesTo(AppScope::class) +@BindingContainer +interface DeveloperSettingsModule { + @Binds + fun bindAppDeveloperSettingsPresenter(presenter: AppDeveloperSettingsPresenter): Presenter +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index bddae2fffb..1ab69f6007 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -103,6 +104,14 @@ class EditUserProfilePresenter( } } + val homeserverCapabilities = matrixClient.homeserverCapabilities() + val canChangeDisplayName = produceState(true) { + value = homeserverCapabilities.canChangeDisplayName().getOrDefault(true) + } + val canChangeAvatar = produceState(true) { + value = homeserverCapabilities.canChangeAvatarUrl().getOrDefault(true) + } + val saveAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val localCoroutineScope = rememberCoroutineScope() @@ -169,6 +178,8 @@ class EditUserProfilePresenter( saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading, saveAction = saveAction.value, cameraPermissionState = cameraPermissionState, + canChangeDisplayName = canChangeDisplayName.value, + canChangeAvatarUrl = canChangeAvatar.value, eventSink = ::handleEvent, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt index a638ed8378..a40f1710e2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -22,5 +22,7 @@ data class EditUserProfileState( val saveButtonEnabled: Boolean, val saveAction: AsyncAction, val cameraPermissionState: PermissionsState, + val canChangeDisplayName: Boolean, + val canChangeAvatarUrl: Boolean, val eventSink: (EditUserProfileEvent) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt index ca9571aea5..13f69a7e1e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -22,6 +22,7 @@ open class EditUserProfileStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false), + canChangeDisplayName: Boolean = true, + canChangeAvatarUrl: Boolean = true, eventSink: (EditUserProfileEvent) -> Unit = {}, ) = EditUserProfileState( userId = userId, @@ -42,5 +45,7 @@ fun aEditUserProfileState( saveButtonEnabled = saveButtonEnabled, saveAction = saveAction, cameraPermissionState = cameraPermissionState, + canChangeDisplayName = canChangeDisplayName, + canChangeAvatarUrl = canChangeAvatarUrl, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index 774dcedae0..d4571d7be5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -120,6 +120,7 @@ fun EditUserProfileView( state = avatarPickerState, onClick = ::onAvatarClick, modifier = Modifier.align(Alignment.CenterHorizontally), + enabled = state.canChangeAvatarUrl, ) Spacer(modifier = Modifier.height(16.dp)) Text( @@ -134,6 +135,7 @@ fun EditUserProfileView( value = state.displayName, placeholder = stringResource(CommonStrings.common_room_name_placeholder), singleLine = true, + enabled = state.canChangeDisplayName, onValueChange = { state.eventSink(EditUserProfileEvent.UpdateDisplayName(it)) }, ) } diff --git a/features/preferences/impl/src/main/res/values-ja/translations.xml b/features/preferences/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..9cd0dfea96 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,82 @@ + + + "重要な電話を確実に受け取るため、端末がロックされている状態での全画面通知を、設定から許可してください。" + "通話品質を高める" + "通知の受信方法を選択してください" + "開発者モード" + "開発者向けの機能を表示します。" + "任意の Element Call のベースURL" + "任意の Element Call のベースURLを入力してください。" + "無効なURLです。プロトコル (http/https) が明記されていることと、アドレスが正しいことを確認してください。" + "ルームへの招待リクエストにアバターを表示しない" + "タイムラインにメディアのプレビューを表示しない" + "ラボ" + "写真や動画を高速で送信してデータ使用量を減らす" + "メディアの品質を最適化" + "制限と安全" + "自動的に画像を最適化してアップロード時間とファイルサイズを削減します。" + "画像のアップロード画質を最適化" + "%1$s 変更するにはタップしてください。" + "高画質 (1080p)" + "低画質 (480p)" + "標準 (720p)" + "動画のアップロード品質" + "プッシュ通知プロバイダー" + "リッチテキスト編集機能を無効化し、Markdown記法を手入力できるようにします。" + "既読を通知" + "機能がオフの場合、メッセージを確認したことを誰にも通知しません。他のユーザーの既読は確認することができます。" + "在席を共有" + "機能がオフの場合、既読の情報と入力中の通知を使用できなくなります。" + "常に非表示" + "常に表示" + "非公開ルームのみ" + "非表示のメディアはタップして表示することができます。" + "タイムラインにメディアを表示" + "タイムラインでメッセージのソースを表示する機能を追加します。" + "ブロックしたユーザーはいません" + "ブロックを解除" + "すべてのメッセージが再表示されます。" + "ユーザーのブロックを解除" + "ブロック解除中…" + "表示名" + "あなたの表示名" + "不明な問題が発生したため、情報の更新に失敗しました。" + "プロフィールを更新できません" + "プロフィールを編集" + "プロフィールを更新中…" + "スレッドへの返信を有効化" + "変更を適用するためにアプリケーションは再起動します。" + "開発段階の最新機能を試します。未完成のため変更や不安定な挙動を生じる可能性があります。" + "探究したいですか?" + "ラボ" + "追加設定" + "音声・ビデオ通話" + "設定の不一致" + "通知設定を簡素化し、見つけやすくしました。以前のカスタム設定の一部はここに表示されませんが、引き続き有効です。 + +続行すると、一部の設定が変更される可能性があります。" + "ダイレクトチャット" + "チャットごとのカスタム設定" + "通知設定の更新中に問題が発生しました。" + "すべてのメッセージ" + "メンションとキーワードのみ" + "ダイレクトチャットで以下の通知を受け取る" + "グループチャットでは以下の通知を受け取る" + "この端末で通知を受け取る" + "設定が修正されていません。再試行してください。" + "グループチャット" + "招待" + "暗号化されたルームでは、この機能にホームサーバーが対応しないため、一部のルームから通知が届かない可能性があります。" + "メンション" + "すべて" + "メンション" + "以下を通知する" + @ルームで通知を受け取る + "通知を受け取るには、%1$s を変更してください。" + "システム設定" + "システムで通知がオフです" + "通知" + "プッシュ履歴" + "トラブルシューティング" + "通知のトラブルシューティング" + diff --git a/features/preferences/impl/src/main/res/values-ru/translations.xml b/features/preferences/impl/src/main/res/values-ru/translations.xml index bf9d268b45..77be6fd8db 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -25,7 +25,7 @@ "Отключить редактор форматированного текста и включить Markdown." "Уведомления о прочтении" "Если этот параметр выключен, другие пользователи не будут видеть, прочитали ли вы сообщения. Вы по-прежнему будете видеть статус прочтения других пользователей." - "Поделиться присутствием" + "Делиться присутствием" "Если выключено, вы не будете видеть, кто печатает и читает сообщения, а также другие пользователи не будут знать, когда вы печатаете или читаете сообщения." "Всегда скрывать" "Всегда показывать" @@ -44,7 +44,7 @@ "Невозможно обновить профиль" "Редактировать профиль" "Обновление профиля…" - "Включить ответы в ветке" + "Включить ответы в обсуждениях" "Приложение перезапустится, чтобы применить это изменение." "Попробуйте функции в разработке. Эти функции ещё не завершены, они нестабильны и могут измениться." "Хотите попробовать?" diff --git a/features/preferences/impl/src/main/res/values-vi/translations.xml b/features/preferences/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..db242130ed --- /dev/null +++ b/features/preferences/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,61 @@ + + + "Chọn cách nhận thông báo" + "Chế độ nhà phát triển" + "Cho phép truy cập vào các tính năng và chức năng dành cho nhà phát triển." + "Địa chỉ tùy chỉnh máy chủ cuộc gọi Element" + "Đặt địa chỉ máy chủ cuộc gọi tùy chỉnh cho Element." + "Địa chỉ URL không hợp lệ. Hãy kiểm tra lại giao thức (http/https) và địa chỉ chính xác." + "Labs" + "Tắt trình soạn thảo văn bản nâng cao để nhập Markdown thủ công." + "Thông báo đã đọc" + "Nếu tắt, thông báo đã đọc của bạn sẽ không được gửi cho ai. Bạn vẫn sẽ nhận được thông báo đã đọc từ người khác." + "Chia sẻ trạng thái" + "Nếu tắt, bạn sẽ không thể gửi hoặc nhận thông báo đã đọc và thông báo \"đang nhập…\"." + "Bật tùy chọn xem nguồn tin nhắn trong dòng thời gian." + "Bạn chưa chặn ai cả" + "Bỏ chặn" + "Bạn sẽ có thể xem lại tất cả tin nhắn từ họ." + "Bỏ chặn người dùng" + "Đang mở khóa…" + "Tên hiển thị" + "Tên hiển thị của bạn" + "Có lỗi không xác định, thông tin không được cập nhật." + "Không thể cập nhật hồ sơ" + "Chỉnh sửa hồ sơ" + "Đang cập nhật hồ sơ…" + "Cho phép trả lời theo chủ đề" + "Ứng dụng sẽ khởi động lại để áp dụng thay đổi này." + "Hãy thử các ý tưởng mới nhất đang được phát triển. Các tính năng này chưa hoàn thiện; có thể không ổn định và có thể thay đổi." + "Bạn muốn thử tính năng thử nghiệm?" + "Labs" + "Cài đặt bổ sung" + "Cuộc gọi âm thanh và video" + "Cấu hình không khớp" + "Chúng tôi đã đơn giản hóa Cài đặt Thông báo để các tùy chọn dễ tìm hơn. Một số cài đặt tùy chỉnh bạn đã chọn trước đây không hiển thị ở đây, nhưng vẫn đang hoạt động. + +Nếu bạn tiếp tục, một số cài đặt của bạn có thể thay đổi." + "Trò chuyện trực tiếp" + "Cài đặt tùy chỉnh cho từng cuộc trò chuyện" + "Đã xảy ra lỗi khi cập nhật cài đặt thông báo." + "Tất cả tin nhắn." + "Chỉ đề cập và từ khóa" + "Trong chat riêng, nhắc tôi khi" + "Trong chat nhóm, nhắc tôi khi" + "Bật thông báo trên thiết bị này" + "Cấu hình chưa đúng, hãy thử lại." + "Trò chuyện nhóm" + "Lời mời" + "Máy chủ không hỗ trợ tùy chọn này trong phòng mã hóa, một số phòng có thể không thông báo." + "Nhắc đến" + "Tất cả" + "Nhắc đến" + "Thông báo cho tôi khi" + "Thông báo cho tôi khi @room" + "Để nhận thông báo, vui lòng thay đổi %1$s của bạn." + "cài đặt hệ thống" + "Thông báo hệ thống đã tắt" + "Thông báo" + "Khắc phục sự cố" + "Khắc phục sự cố thông báo" + diff --git a/features/preferences/impl/src/main/res/values-zh/translations.xml b/features/preferences/impl/src/main/res/values-zh/translations.xml index 0c576b58b6..0dac7ab6d8 100644 --- a/features/preferences/impl/src/main/res/values-zh/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh/translations.xml @@ -6,7 +6,7 @@ "开发者模式" "允许开发人员访问特性和功能。" "自定义 Element Call URL" - "为 Element 通话设置根 URL。" + "为 Element 通话设置基本 URL。" "URL 无效,请确保包含协议(http/https)和正确的地址。" "在房间邀请请求中隐藏头像" "在时间轴中隐藏媒体预览" @@ -34,12 +34,12 @@ "在时间轴中显示媒体" "启用在时间轴中查看消息源码的选项。" "您没有屏蔽用户" - "解封" + "解除屏蔽" "可以重新接收他们的消息。" - "解封用户" + "解除屏蔽用户" "正在解除屏蔽……" "显示名称" - "你的显示名称" + "您的显示名称" "遇到未知错误,无法更改信息。" "无法更新个人资料" "编辑个人资料" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 1fcf9bff70..ec70b19eab 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -14,27 +14,18 @@ import androidx.compose.ui.graphics.Color import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase -import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.data.megaBytes -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.featureflag.api.Feature -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeature -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -51,17 +42,7 @@ class DeveloperSettingsPresenterTest { @Test fun `present - ensures initial states are correct`() = runTest { - val getAvailableFeaturesResult = lambdaRecorder> { _, _ -> - listOf( - FakeFeature( - key = "feature_1", - title = "Feature 1", - isInLabs = false, - ) - ) - } val presenter = createDeveloperSettingsPresenter( - featureFlagService = FakeFeatureFlagService(getAvailableFeaturesResult = getAvailableFeaturesResult), databaseSizesUseCase = GetDatabaseSizesUseCase { Result.success( SdkStoreSizes(stateStore = 10.megaBytes, eventCacheStore = 10.megaBytes, mediaStore = 10.megaBytes, cryptoStore = 10.megaBytes) @@ -70,22 +51,14 @@ class DeveloperSettingsPresenterTest { ) presenter.test { awaitItem().also { state -> - assertThat(state.features).isEmpty() + assertThat(state.appDeveloperSettingsState.features).isNotEmpty() assertThat(state.clearCacheAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized) - assertThat(state.customElementCallBaseUrlState).isNotNull() - assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() - assertThat(state.rageshakeState.isEnabled).isFalse() - assertThat(state.rageshakeState.isSupported).isTrue() - assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) - assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized) assertThat(state.isEnterpriseBuild).isFalse() assertThat(state.showColorPicker).isFalse() } awaitItem().also { state -> - assertThat(state.features).isNotEmpty() - assertThat(state.features).hasSize(1) - assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) + assertThat(state.cacheSize.isLoading()).isTrue() } awaitItem().also { state -> assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java) @@ -98,37 +71,6 @@ class DeveloperSettingsPresenterTest { ) ) } - getAvailableFeaturesResult.assertions().isCalledOnce() - .with(value(false), value(false)) - } - } - - @Test - fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest { - val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay") - val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch) - } - } - } - - @Test - fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { - val presenter = createDeveloperSettingsPresenter() - presenter.test { - skipItems(2) - awaitItem().also { state -> - val feature = state.features.first { !it.isEnabled } - state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled)) - } - awaitItem().also { state -> - val feature = state.features.first() - assertThat(feature.isEnabled).isTrue() - assertThat(feature.key).isEqualTo(feature.key) - } } } @@ -158,52 +100,6 @@ class DeveloperSettingsPresenterTest { } } - @Test - fun `present - custom element call base url`() = runTest { - val preferencesStore = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() - state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy")) - } - awaitItem().also { state -> - assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy") - } - } - } - - @Test - fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest { - val presenter = createDeveloperSettingsPresenter() - presenter.test { - skipItems(2) - val urlValidator = awaitItem().customElementCallBaseUrlState.validator - assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one - assertThat(urlValidator("test")).isFalse() - assertThat(urlValidator("http://")).isFalse() - assertThat(urlValidator("geo://test")).isFalse() - assertThat(urlValidator("https://call.element.io")).isTrue() - } - } - - @Test - fun `present - changing tracing log level`() = runTest { - val preferences = InMemoryAppPreferencesStore() - val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences) - presenter.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) - state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.TRACE)) - } - awaitItem().also { state -> - assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.TRACE) - } - } - } - @Test fun `present - enterprise build can change the brand color`() = runTest { val overrideBrandColorResult = lambdaRecorder { _, _ -> } @@ -250,33 +146,17 @@ class DeveloperSettingsPresenterTest { private fun createDeveloperSettingsPresenter( sessionId: SessionId = A_SESSION_ID, - featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( - getAvailableFeaturesResult = { _, _ -> - listOf( - FakeFeature( - key = "feature_1", - title = "Feature 1", - isInLabs = false, - ) - ) - } - ), cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), - preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), - buildMeta: BuildMeta = aBuildMeta(), enterpriseService: EnterpriseService = FakeEnterpriseService(), vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {}, databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) }, ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( + appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() }, sessionId = sessionId, - featureFlagService = featureFlagService, computeCacheSizeUseCase = cacheSizeUseCase, clearCacheUseCase = clearCacheUseCase, - rageshakePresenter = { aRageshakePreferencesState() }, - appPreferencesStore = preferencesStore, - buildMeta = buildMeta, enterpriseService = enterpriseService, vacuumStoresUseCase = vacuumStoresUseCase, databaseSizesUseCase = databaseSizesUseCase, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index 3854e3f4a1..d4d02d7de9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -9,20 +9,12 @@ package io.element.android.features.preferences.impl.developer import androidx.activity.ComponentActivity -import androidx.compose.ui.test.filterToOne -import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.isDialog -import androidx.compose.ui.test.isEditable -import androidx.compose.ui.test.isFocusable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R -import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem -import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn @@ -53,7 +45,7 @@ class DeveloperSettingsViewTest { } } - @Config(qualifiers = "h1500dp") + @Config(qualifiers = "h2000dp") @Test fun `clicking on push history notification invokes the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -68,22 +60,6 @@ class DeveloperSettingsViewTest { } } - @Config(qualifiers = "h1500dp") - @Test - fun `clicking on element call url open the dialogs and submit emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) - val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) - textInputNode.performTextInput("https://call.element.dev") - rule.clickOn(CommonStrings.action_ok) - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) - } - @Config(qualifiers = "h2000dp") @Test fun `clicking on open showkase invokes the expected callback`() { @@ -99,20 +75,6 @@ class DeveloperSettingsViewTest { } } - @Config(qualifiers = "h1024dp") - @Test - fun `clicking on log level emits the expected event`() { - val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( - state = aDeveloperSettingsState( - eventSink = eventsRecorder - ), - ) - rule.onNodeWithText("Tracing log level").performClick() - rule.onNodeWithText("Debug").performClick() - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.DEBUG)) - } - @Config(qualifiers = "h2200dp") @Test fun `clicking on clear cache emits the expected event`() { diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt new file mode 100644 index 0000000000..123f31ae8e --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPageTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.impl.developer.appsettings + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.isEditable +import androidx.compose.ui.test.isFocusable +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.preferences.impl.R +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class AppDeveloperSettingsPageTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Config(qualifiers = "h1500dp") + @Test + fun `clicking on element call url open the dialogs and submit emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) + textInputNode.performTextInput("https://call.element.dev") + rule.clickOn(CommonStrings.action_ok) + eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.dev")) + } + + @Config(qualifiers = "h2000dp") + @Test + fun `clicking on open showkase invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + onOpenShowkase = it + ) + rule.onNodeWithText("Open Showkase browser").performClick() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on log level emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setAppDeveloperSettingsView( + state = anAppDeveloperSettingsState( + eventSink = eventsRecorder + ), + ) + rule.onNodeWithText("Tracing log level").performClick() + rule.onNodeWithText("Debug").performClick() + eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG)) + } +} + +private fun AndroidComposeTestRule.setAppDeveloperSettingsView( + state: AppDeveloperSettingsState, + onOpenShowkase: () -> Unit = EnsureNeverCalled(), + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + AppDeveloperSettingsPage( + state = state, + onOpenShowkase = onOpenShowkase, + onBackClick = onBackClick, + ) + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt new file mode 100644 index 0000000000..0e9d774e84 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsPresenterTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.preferences.impl.developer.appsettings + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem +import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.featureflag.api.Feature +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeature +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class AppDeveloperSettingsPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - ensures initial states are correct`() = runTest { + val getAvailableFeaturesResult = lambdaRecorder> { _, _ -> + listOf( + FakeFeature( + key = "feature_1", + title = "Feature 1", + isInLabs = false, + ) + ) + } + val presenter = createAppDeveloperSettingsPresenter( + featureFlagService = FakeFeatureFlagService(getAvailableFeaturesResult = getAvailableFeaturesResult), + ) + presenter.test { + awaitItem().also { state -> + assertThat(state.features).isEmpty() + assertThat(state.customElementCallBaseUrlState).isNotNull() + assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() + assertThat(state.rageshakeState.isEnabled).isFalse() + assertThat(state.rageshakeState.isSupported).isTrue() + assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) + assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized) + } + awaitItem().also { state -> + assertThat(state.features).isNotEmpty() + assertThat(state.features).hasSize(1) + assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) + } + getAvailableFeaturesResult.assertions().isCalledOnce() + .with(value(false), value(false)) + } + } + + @Test + fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest { + val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay") + val presenter = createAppDeveloperSettingsPresenter(buildMeta = buildMeta) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch) + } + } + } + + @Test + fun `present - ensures state is updated when enabled feature event is triggered`() = runTest { + val presenter = createAppDeveloperSettingsPresenter() + presenter.test { + skipItems(1) + awaitItem().also { state -> + val feature = state.features.first { !it.isEnabled } + state.eventSink(AppDeveloperSettingsEvent.UpdateEnabledFeature(feature, !feature.isEnabled)) + } + awaitItem().also { state -> + val feature = state.features.first() + assertThat(feature.isEnabled).isTrue() + assertThat(feature.key).isEqualTo(feature.key) + } + } + } + + @Test + fun `present - custom element call base url`() = runTest { + val preferencesStore = InMemoryAppPreferencesStore() + val presenter = createAppDeveloperSettingsPresenter(preferencesStore = preferencesStore) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.customElementCallBaseUrlState.baseUrl).isNull() + state.eventSink(AppDeveloperSettingsEvent.SetCustomElementCallBaseUrl("https://call.element.ahoy")) + } + awaitItem().also { state -> + assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy") + } + } + } + + @Test + fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest { + val presenter = createAppDeveloperSettingsPresenter() + presenter.test { + skipItems(1) + val urlValidator = awaitItem().customElementCallBaseUrlState.validator + assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one + assertThat(urlValidator("test")).isFalse() + assertThat(urlValidator("http://")).isFalse() + assertThat(urlValidator("geo://test")).isFalse() + assertThat(urlValidator("https://call.element.io")).isTrue() + } + } + + @Test + fun `present - changing tracing log level`() = runTest { + val preferences = InMemoryAppPreferencesStore() + val presenter = createAppDeveloperSettingsPresenter(preferencesStore = preferences) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO) + state.eventSink(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.TRACE)) + } + awaitItem().also { state -> + assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.TRACE) + } + } + } + + private fun createAppDeveloperSettingsPresenter( + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( + getAvailableFeaturesResult = { _, _ -> + listOf( + FakeFeature( + key = "feature_1", + title = "Feature 1", + isInLabs = false, + ) + ) + } + ), + preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), + buildMeta: BuildMeta = aBuildMeta(), + ): AppDeveloperSettingsPresenter { + return AppDeveloperSettingsPresenter( + featureFlagService = featureFlagService, + rageshakePresenter = { aRageshakePreferencesState() }, + appPreferencesStore = preferencesStore, + buildMeta = buildMeta, + ) + } +} diff --git a/features/preferences/test/build.gradle.kts b/features/preferences/test/build.gradle.kts new file mode 100644 index 0000000000..7e3da4a6e8 --- /dev/null +++ b/features/preferences/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.preferences.test" +} + +dependencies { + implementation(projects.features.preferences.api) + implementation(projects.tests.testutils) +} diff --git a/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakePreferencesEntryPoint.kt b/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakePreferencesEntryPoint.kt new file mode 100644 index 0000000000..c57ed434fa --- /dev/null +++ b/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakePreferencesEntryPoint.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.preferences.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakePreferencesEntryPoint : PreferencesEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + params: PreferencesEntryPoint.Params, + callback: PreferencesEntryPoint.Callback, + ): Node { + lambdaError() + } + + override fun createAppDeveloperSettingsNode( + parentNode: Node, + buildContext: BuildContext, + callback: PreferencesEntryPoint.DeveloperSettingsCallback, + ): Node { + lambdaError() + } +} diff --git a/features/rageshake/api/src/main/res/values-ja/translations.xml b/features/rageshake/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..7afb523cd1 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s は前回の利用時にクラッシュしました。クラッシュレポートを開発者に共有しますか?" + "怒り狂って端末を振っていますね。バグの報告をしますか?" + "怒り狂う" + "検出感度" + diff --git a/features/rageshake/api/src/main/res/values-lt/translations.xml b/features/rageshake/api/src/main/res/values-lt/translations.xml index 49762d57ca..88c49aca3c 100644 --- a/features/rageshake/api/src/main/res/values-lt/translations.xml +++ b/features/rageshake/api/src/main/res/values-lt/translations.xml @@ -2,6 +2,6 @@ "%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?" "Atrodo, kad nusivylęs purtote telefoną. Ar norėtumėte atidaryti pranešimo apie klaidas ekraną?" - "Rageshake" + "Gerai pakratyti" "Aptikimo riba" diff --git a/features/rageshake/api/src/main/res/values-vi/translations.xml b/features/rageshake/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..086619eed0 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s đã bị lỗi ở lần sử dụng gần nhất. Bạn có muốn chia sẻ báo cáo lỗi với chúng tôi không?" + "Có vẻ như bạn đang lắc điện thoại vì bực bội. Bạn có muốn mở màn hình báo cáo lỗi không?" + "Lắc điện thoại" + "Ngưỡng phát hiện" + diff --git a/features/rageshake/impl/src/main/res/values-ja/translations.xml b/features/rageshake/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..7c46d2beb5 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,20 @@ + + + "スクリーンショットを添付" + "追加の質問がある場合は、ご連絡ください。" + "返信を受け取る" + "スクリーンショットを編集" + "問題を教えてください。行った操作、想定した挙動、実際の挙動などについて、可能な限り詳細に記述してください。" + "問題の説明…" + "可能であれば英語で記入してください。" + "説明が過度に短いです。問題についてより詳細にご記入ください。" + "クラッシュログを送信" + "ログの記録を許可" + "ログのサイズが大きく、報告に添付することができません。ログは別の方法で送信してください。" + "スクリーンを送信" + "メッセージには、正常に動作していることを確認するため、ログが含まれています。ログを含めたくない場合は、オフにしてください。" + "%1$s は前回の利用時にクラッシュしました。クラッシュレポートを開発者に共有しますか?" + "通知について問題がある場合は、プッシュ通知の設定を添付することで、原因究明の手がかりになります。この設定には、ユーザーネームや通知のキーワードなどの個人情報が含まれる場合があります。ご注意ください。" + "通知設定を送信" + "ログを表示" + diff --git a/features/rageshake/impl/src/main/res/values-lt/translations.xml b/features/rageshake/impl/src/main/res/values-lt/translations.xml index c45c982ba5..cc79a5055e 100644 --- a/features/rageshake/impl/src/main/res/values-lt/translations.xml +++ b/features/rageshake/impl/src/main/res/values-lt/translations.xml @@ -10,6 +10,6 @@ "Siųsti gedimų žurnalus" "Leisti žurnalus" "Siųsti ekrano nuotrauką" - "Prie žinutės bus pridėti žurnalai, kad įsitikintumėme, jog viskas veikia tinkamai. Jei norite išsiųsti savo žinutę be žurnalų, išjunkite šį nustatymą." + "Žurnalai bus įtraukti į jūsų žinutę, kad būtų užtikrinta, jog viskas veikia tinkamai. Kad išsiųstumėte žinutę be žurnalų, išjunkite šį nustatymą." "%1$s nulūžo paskutinį kartą, kai buvo naudojama. Ar norėtumėte su mumis pasidalyti strigčių ataskaita?" diff --git a/features/rageshake/impl/src/main/res/values-vi/translations.xml b/features/rageshake/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5a60edd206 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,17 @@ + + + "Đính kèm ảnh chụp màn hình" + "Bạn có thể liên hệ với tôi nếu có bất kỳ câu hỏi nào khác." + "Liên hệ với tôi" + "Chỉnh sửa ảnh chụp màn hình" + "Hãy mô tả vấn đề. Bạn đã làm gì? Bạn mong đợi/dự đoán điều gì sẽ xảy ra? Điều gì thực sự đã xảy ra? Hãy trình bày càng chi tiết càng tốt." + "Hãy mô tả vấn đề…" + "Nếu có thể, vui lòng viết mô tả bằng tiếng Anh." + "Phần mô tả quá ngắn, vui lòng cung cấp thêm chi tiết về những gì đã xảy ra. Cảm ơn!" + "Gửi nhật ký sự cố" + "Cho phép ghi nhật ký" + "Gửi ảnh chụp màn hình" + "Nhật ký lỗi sẽ được đính kèm với tin nhắn của bạn để đảm bảo mọi thứ hoạt động bình thường. Để gửi tin nhắn mà không có nhật ký lỗi, hãy tắt cài đặt này." + "%1$s đã bị lỗi ở lần sử dụng gần nhất. Bạn có muốn chia sẻ báo cáo lỗi với chúng tôi không?" + "Xem nhật ký" + diff --git a/features/reportroom/impl/src/main/res/values-ja/translations.xml b/features/reportroom/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..9718ca432f --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + "報告は正常に送信されましたが、ルームの退出中に問題が発生しました。もう一度試してください。" + "ルームの退出に失敗" + "管理者にこのルームを報告します。メッセージが暗号化されている場合、管理者は内容を確認することができません。" + "報告の理由を説明してください…" + "ルームを通報" + diff --git a/features/reportroom/impl/src/main/res/values-vi/translations.xml b/features/reportroom/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..3037521f98 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Hãy báo cáo phòng chat này cho quản trị viên của bạn. Nếu tin nhắn được mã hóa, quản trị viên sẽ không thể đọc được chúng." + "Báo cáo phòng" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c9410e9e3c --- /dev/null +++ b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,82 @@ + + + "管理者" + "ユーザーの追放" + "設定の変更" + "メッセージの削除" + "メンバー" + "ユーザーの招待" + "スペースの管理" + "ルームを管理" + "メンバーの管理" + "メッセージと内容" + "モデレーター" + "ユーザーの削除" + "アバターの変更" + "詳細を編集" + "名前の変更" + "トピックの変更" + "メッセージの送信" + "権限" + "管理者を編集" + "この操作は取り消せません。このユーザーをあなたと同じ権限まで昇格します。" + "管理者を追加しますか?" + "この操作は取り消せません。選択したユーザーに所有権を譲与します。あなたがルームを退出すると恒久的に変更が適用されます。" + "所有権を譲与しますか?" + "降格" + "自身を降格しようとしているため、後から取り消すことはできません。このルームに他に特権を持つユーザーが存在しない場合、それを回復することはできなくなります。" + "自身を降格しますか?" + "%1$s (承認待ち)" + "承認待ち" + "管理者はモデレータの特権を有します。" + "所有者は管理者の特権を有します。" + "モデレーターを編集" + "所有者を選択" + "管理者" + "モデレーター" + "メンバー" + "未保存の変更内容があります。" + "変更を保存しますか?" + "追放されたユーザーはいません。" + + "%1$d 人の追放" + + "スペルを確認するか、新たに検索し直してください" + "\"%1$s\" の検索結果はありません" + + "%1$d 人" + + "ユーザーを追放" + "メンバーのみを削除" + "追放を解除" + "招待を受け取ると再度参加できます。" + "ユーザーの追放を解除" + "追放済み" + "メンバー" + + "%1$d 件の招待" + + "待機中" + "管理者" + "モデレーター" + "所有者" + "ルームのメンバー" + "%1$s の追放を解除中" + "管理者" + "管理者と所有者" + "自身の役割を変更" + "権限を譲与" + "モデレーターに譲与" + "メンバーの編集" + "メッセージと内容" + "モデレーター" + "所有者" + "権限" + "権限をリセット" + "権限をリセットすると現在の設定はすべて失われます。" + "権限をリセットしますか?" + "役割" + "ルームの詳細" + "スペースの詳細" + "役割と権限" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5e6c736cbe --- /dev/null +++ b/features/rolesandpermissions/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,58 @@ + + + "Quản trị viên" + "Cấm người dùng" + "Xóa tin nhắn" + "Thành viên" + "Mời mọi người" + "Quản lý phòng trò chuyện" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Gỡ người dùng" + "Đổi ảnh đại diện" + "Chỉnh sửa thông tin" + "Đổi tên" + "Đổi chủ đề" + "Gửi tin nhắn." + "Chỉnh sửa Quản trị viên" + "Bạn sẽ không thể hoàn tác hành động này. Bạn đang thăng quyền cho người dùng lên cùng cấp quyền với bạn." + "Thêm quản trị viên?" + "Giáng cấp" + "Bạn sẽ không thể hoàn tác thay đổi này vì bạn đang tự giáng cấp bản thân, nếu bạn là người dùng cuối cùng có đặc quyền trong phòng, nó sẽ không thể lấy lại đặc quyền." + "Giáng cấp bản thân?" + "%1$s (Đang chờ xử lý)" + "Chỉnh sửa Người điều hành" + "Quản trị viên" + "Người điều hành" + "Thành viên" + "Bạn có thay đổi chưa được lưu." + "Lưu thay đổi?" + "Hiện không có người dùng nào bị cấm." + + "%1$d người" + + "Cấm người dùng" + "Chỉ xóa thành viên" + "Bỏ cấm" + "Họ có thể tham gia lại phòng này nếu được mời." + "Bị cấm" + "Thành viên" + "Quản trị viên" + "Người điều hành" + "Thành viên phòng" + "Đang gỡ cấm %1$s" + "Quản trị viên" + "Thay đổi vai trò của tôi" + "Hạ cấp xuống thành thành viên" + "Hạ cấp xuống làm người điều hành" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Đặt lại quyền truy cập" + "Sau khi bạn đặt lại quyền truy cập, bạn sẽ mất các cài đặt hiện tại." + "Đặt lại quyền truy cập?" + "Vai trò" + "Chi tiết phòng" + "Vai trò và quyền hạn" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml index 768bed1a86..d3c4cada77 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml @@ -19,7 +19,7 @@ "发送消息" "权限" "编辑管理员" - "您将无法撤消此操作。您正在提升用户的权限,使其拥有与您平权。" + "您将无法撤消此操作。您正在提升用户的权限到与您相同的级别。" "添加管理员?" "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" "转让所有权" @@ -50,7 +50,7 @@ "仅移除成员" "取消封禁" "如果受到邀请,他们可以重新加入聊天室。" - "从房间取消解封" + "解封用户" "已封禁用户" "成员" @@ -61,7 +61,7 @@ "协管员" "所有者" "聊天室成员" - "解除封禁 %1$s" + "正在解除封禁 %1$s" "管理员" "管理员和所有者" "更改我的角色" diff --git a/features/roomaliasresolver/impl/src/main/res/values-ja/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..4e3f765f88 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "ルームのプレビューを表示できません" + "ルームエイリアスを解決できません。" + diff --git a/features/roomaliasresolver/impl/src/main/res/values-vi/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b08c97b72a --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "Không thể hiển thị bản xem trước của phòng này" + diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt index 54ffdf6c25..3d863321e3 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenter.kt @@ -20,6 +20,8 @@ import io.element.android.features.call.api.CurrentCallService import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.canCall @@ -57,8 +59,7 @@ class RoomCallStatePresenter( canJoinCall = canJoinCall, isUserInTheCall = isUserInTheCall, isUserLocallyInTheCall = isUserLocallyInTheCall, - // TODO resolve intent while the call is ongoing - isAudioCall = false + isAudioCall = roomInfo.activeCallIntentConsensus.isAudio(), ) else -> RoomCallState.StandBy( canStartCall = canJoinCall, @@ -70,3 +71,12 @@ class RoomCallStatePresenter( return callState } } + +fun CallIntentConsensus.isAudio(): Boolean { + val intent = when (this) { + is CallIntentConsensus.Full -> callIntent + is CallIntentConsensus.Partial -> callIntent + is CallIntentConsensus.None -> return false + } + return intent == CallIntent.AUDIO +} diff --git a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt index 0a561ad59a..4c6fcf8e59 100644 --- a/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt +++ b/features/roomcall/impl/src/test/kotlin/io/element/android/features/roomcall/impl/RoomCallStatePresenterTest.kt @@ -14,6 +14,8 @@ import io.element.android.features.call.api.CurrentCallService import io.element.android.features.call.test.FakeCurrentCallService import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.roomcall.api.RoomCallState +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.room.CallIntentConsensus import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.test.room.FakeBaseRoom @@ -188,6 +190,100 @@ class RoomCallStatePresenterTest { } } + @Test + fun `present - active call with audio Intent`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(true), + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO), + activeRoomCallParticipants = emptyList(), + ) + ) + } + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isAudioCall = true, + isUserInTheCall = false, + isUserLocallyInTheCall = true, + ) + ) + } + } + + @Test + fun `present - active call with partial audio Intent`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(true), + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeCallIntentConsensus = CallIntentConsensus.Partial(CallIntent.AUDIO, 1, 4), + ) + ) + } + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isAudioCall = true, + isUserInTheCall = false, + isUserLocallyInTheCall = true, + ) + ) + } + } + + @Test + fun `present - active call with no intent defaults to Audio`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(true), + ).apply { + givenRoomInfo( + aRoomInfo( + hasRoomCall = true, + activeCallIntentConsensus = CallIntentConsensus.None, + ) + ) + } + ) + val presenter = createRoomCallStatePresenter( + joinedRoom = room, + currentCallService = FakeCurrentCallService(MutableStateFlow(CurrentCall.RoomCall(room.roomId))), + ) + presenter.test { + skipItems(1) + assertThat(awaitItem()).isEqualTo( + RoomCallState.OnGoing( + canJoinCall = true, + isAudioCall = false, + isUserInTheCall = false, + isUserLocallyInTheCall = true, + ) + ) + } + } + @Test fun `present - user leaves the call`() = runTest { val room = FakeJoinedRoom( diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 928c08324d..07e10c65ef 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -39,6 +39,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun navigateToGlobalNotificationSettings() + fun navigateToDeveloperSettings() fun navigateToRoom(roomId: RoomId, serverNames: List) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index e1024c611f..c3ae902ba9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -388,6 +388,10 @@ class RoomDetailsFlowNode( override fun navigateToRoom(roomId: RoomId) { callback.navigateToRoom(roomId, emptyList()) } + + override fun navigateToDeveloperSettings() { + callback.navigateToDeveloperSettings() + } } return messagesEntryPoint.createNode( parentNode = this, diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index 053e269bc4..22ec99f2b5 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -1,5 +1,8 @@ + "I nuovi membri non vedono la cronologia" + "I nuovi membri vedono la cronologia" + "Chiunque può vedere la cronologia" "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Modifica indirizzo" "Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica." @@ -63,6 +66,7 @@ "Profilo" "Richieste di accesso" "Ruoli e autorizzazioni" + "Nome" "Sicurezza e privacy" "Sicurezza" "Condividi stanza" @@ -128,8 +132,10 @@ "Dettagli della stanza" "Ruoli e autorizzazioni" "Aggiungi indirizzo" + "Chiunque si trovi in spazi autorizzati può partecipare, ma tutti gli altri devono richiedere l\'accesso." "Chiunque deve richiedere l\'accesso." "Chiedi di entrare" + "Chiunque all\'interno di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso." "Sì, attiva la crittografia" "Una volta attivata, la crittografia di una stanza non può essere disattivata, la cronologia dei messaggi sarà visibile solo ai membri della stanza da quando sono stati invitati o da quando sono entrati nella stanza. Nessuno, oltre ai membri della stanza, sarà in grado di leggere i messaggi. Ciò potrebbe impedire ai bot e ai bridge di funzionare correttamente. @@ -140,19 +146,25 @@ Non consigliamo di attivare la crittografia per le stanze che chiunque può trov "Attiva la crittografia end-to-end" "Chiunque può partecipare." "Chiunque" + "Scegli quali membri dello spazio possono accedere a questa stanza senza invito.%1$s" + "Gestisci gli spazi" "Solo le persone invitate possono entrare." "Solo su invito" "Accesso" + "Chiunque si trovi in ​​spazi autorizzati può partecipare." + "Chiunque in %1$s può partecipare." + "Membri dello spazio" "Gli spazi non sono attualmente supportati" "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Indirizzo" "Consenti la ricerca di questa stanza effettuando una ricerca nell\'elenco delle stanze pubbliche di %1$s" "Consenti di essere trovato effettuando una ricerca nell\'elenco pubblico." "Visibile nell\'elenco pubblico" - "Chiunque" + "Chiunque (la cronologia è pubblica)" + "Le modifiche non interesseranno i messaggi passati, ma solo quelli nuovi. %1$s" "Chi può leggere la cronologia messaggi" - "Solo membri da quando sono stati invitati" - "Solo membri da dopo aver selezionato questa opzione" + "Members invited" + "Members (cronologia completa)" "Gli indirizzi delle stanze sono modi per trovare e accedervi. In questo modo puoi anche condividere facilmente la tua stanze con altri. Puoi scegliere di pubblicare la tua stanza nell\'elenco delle stanza pubbliche dell\'homeserver." "Pubblicazione della stanza" diff --git a/features/roomdetails/impl/src/main/res/values-ja/translations.xml b/features/roomdetails/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..70b3255437 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,171 @@ + + + "新しいメンバーは過去の内容を確認できない" + "新しいメンバーは過去の内容を確認できる" + "すべてのユーザーが過去の内容を確認できる" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレスの編集" + "通知設定の更新中に問題が発生しました。" + "暗号化されたルームでは、この機能にホームサーバーが対応しないため、一部のルームから通知が届かない可能性があります。" + "投票" + "管理者" + "ユーザーの追放" + "メッセージの削除" + "メンバー" + "ユーザーの招待" + "メンバーの管理" + "メッセージと内容" + "モデレーター" + "ユーザーの削除" + "アバターの変更" + "詳細を編集" + "名前の変更" + "トピックの変更" + "メッセージの送信" + "管理者を編集" + "この操作は取り消せません。このユーザーをあなたと同じ権限まで昇格します。" + "管理者を追加しますか?" + "この操作は取り消せません。選択したユーザーに所有権を譲与します。あなたがルームを退出すると恒久的に変更が適用されます。" + "所有権を譲与しますか?" + "降格" + "自身を降格しようとしているため、後から取り消すことはできません。このルームに他に特権を持つユーザーが存在しない場合、それを回復することはできなくなります。" + "自身を降格しますか?" + "%1$s (承認待ち)" + "承認待ち" + "管理者はモデレータの特権を有します。" + "所有者は管理者の特権を有します。" + "モデレーターを編集" + "所有者を選択" + "管理者" + "モデレーター" + "メンバー" + "未保存の変更内容があります。" + "変更を保存しますか?" + "トピックを追加" + "暗号化済み" + "暗号化されていません" + "公開ルーム" + "詳細を編集" + "不明な問題が発生したため、情報の更新に失敗しました。" + "ルームを更新することができません" + "メッセージは、あなたと受信者のみが持つ鍵で暗号化されています。" + "メッセージの暗号化が有効です" + "通知設定の読み込み中に問題が発生しました。" + "ルームをミュートできませんでした。再試行してください。" + "ルームをミュート解除できませんでした。再試行してください。" + "終了するまでアプリを閉じないでください。" + "招待を準備中…" + "ユーザーを招待" + "会話を退出" + "ルームを退出" + "ファイルとメディア" + "カスタム" + "デフォルト" + "通知" + "ピン留めされたメッセージ" + "プロフィール" + "参加のリクエスト" + "役割と権限" + "名前" + "セキュリティとプライバシー" + "セキュリティ" + "ルームを共有" + "ルームの情報" + "トピック" + "詳細を更新中…" + "追放されたユーザーはいません。" + + "%1$d 人の追放" + + "スペルを確認するか、新たに検索し直してください" + "\"%1$s\" の検索結果はありません" + + "%1$d 人" + + "ユーザーを追放" + "メンバーのみを削除" + "追放を解除" + "招待を受け取ると再度参加できます。" + "ユーザーの追放を解除" + "追放済み" + "メンバー" + + "%1$d 件の招待" + + "待機中" + "管理者" + "モデレーター" + "所有者" + "ルームのメンバー" + "%1$s の追放を解除中" + "カスタム設定を許可" + "オンにするとデフォルト設定が上書きされます" + "このチャットで以下の通知を受け取る" + "%1$s から変更できます。" + "全体設定" + "デフォルト設定" + "カスタム設定を削除する" + "通知設定の読み込み中に問題が発生しました。" + "デフォルトの復元に失敗しました。再試行してください。" + "設定に失敗しました。再試行してください。" + "暗号化されたルームでは、この機能にホームサーバーが対応しないため、このルームからの通知を受信できません。" + "すべてのメッセージ" + "メンションとキーワードのみ" + "このルームでは以下の通知を受け取る" + "管理者" + "管理者と所有者" + "自身の役割を変更" + "権限を譲与" + "モデレーターに譲与" + "メンバーの編集" + "メッセージと内容" + "モデレーター" + "所有者" + "権限" + "権限をリセット" + "権限をリセットすると現在の設定はすべて失われます。" + "権限をリセットしますか?" + "役割" + "ルームの詳細" + "役割と権限" + "アドレスを追加" + "認証済みのスペースに所属するユーザーのみが参加できます。それ以外のユーザーは参加へのリクエストが必要です。" + "参加のリクエストが必須です。" + "参加をリクエスト" + "%1$s に所属するユーザーのみが参加できます。それ以外のユーザーは参加のリクエストが必要です。" + "暗号化を有効にする" + "暗号化が有効のルームを再び無効化することはできません。過去のメッセージの参照は、ユーザーが招待された、あるいは参加した以降に投稿された内容に限定されます。 +ルームのメンバー以外がメッセージを確認することはできないため、bot やブリッジのサービスが正常に動作しない可能性があります。 +公開スペースを暗号化することは一般に推奨されません。" + "暗号化を有効にしますか?" + "一度有効にすると元に戻すことはできません。" + "暗号化" + "エンドツーエンド暗号化を有効にする" + "誰でも参加できます" + "全員" + "招待無しで参加できるユーザーが所属するルームを選択してください。%1$s" + "スペースを管理" + "招待されたユーザーのみ参加できます。" + "招待制" + "アクセス" + "認証済みのスペースに所属するすべてのユーザーが参加できます。" + "%1$s に所属するすべてのユーザーが参加できます。" + "スペースのメンバー" + "スペースは現在対応していません。" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレス" + "%1$s の公開ルームの検索結果に、このルームを表示します" + "公開ディレクトリの検索結果に表示" + "公開ディレクトリに表示" + "全員(履歴を公開)" + "過去のメッセージに変更は適用されません。新規のメッセージにのみ適用されます。%1$s" + "履歴を表示するユーザー" + "招待済みのユーザー" + "ユーザー (すべての履歴)" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。 +ホームサーバーの公開ディレクトリにルームを表示するかを設定できます。" + "ルームの公開" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。" + "視認性" + "セキュリティとプライバシー" + diff --git a/features/roomdetails/impl/src/main/res/values-vi/translations.xml b/features/roomdetails/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..a2ee35b563 --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,107 @@ + + + "Thành viên mới không thể xem lịch sử" + "Thành viên mới có thể xem lịch sử" + "Ai cũng có thể xem lịch sử" + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + "Chỉnh sửa địa chỉ" + "Đã xảy ra lỗi khi cập nhật cài đặt thông báo." + "Máy chủ không hỗ trợ tùy chọn này trong phòng mã hóa, một số phòng có thể không thông báo." + "Cuộc thăm dò ý kiến" + "Quản trị viên" + "Cấm người dùng" + "Xóa tin nhắn" + "Thành viên" + "Mời mọi người" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Gỡ người dùng" + "Đổi ảnh đại diện" + "Chỉnh sửa thông tin" + "Đổi tên" + "Đổi chủ đề" + "Gửi tin nhắn." + "Chỉnh sửa Quản trị viên" + "Bạn sẽ không thể hoàn tác hành động này. Bạn đang thăng quyền cho người dùng lên cùng cấp quyền với bạn." + "Thêm quản trị viên?" + "Giáng cấp" + "Bạn sẽ không thể hoàn tác thay đổi này vì bạn đang tự giáng cấp bản thân, nếu bạn là người dùng cuối cùng có đặc quyền trong phòng, nó sẽ không thể lấy lại đặc quyền." + "Giáng cấp bản thân?" + "%1$s (Đang chờ xử lý)" + "Chỉnh sửa Người điều hành" + "Quản trị viên" + "Người điều hành" + "Thành viên" + "Bạn có thay đổi chưa được lưu." + "Lưu thay đổi?" + "Thêm chủ đề" + "Chỉnh sửa thông tin" + "Có lỗi không xác định, thông tin không được cập nhật." + "Không thể cập nhật phòng" + "Tin nhắn được bảo mật bằng khóa. Chỉ bạn và người nhận mới có chìa khóa riêng để mở khóa." + "Mã hóa tin nhắn đã được bật" + "Đã xảy ra lỗi khi tải cài đặt thông báo." + "Không thể tắt tiếng phòng này, vui lòng thử lại." + "Không thể bật tiếng cho phòng này. Vui lòng thử lại." + "Mời ai đó" + "Rời khỏi cuộc trò chuyện" + "Rời phòng" + "Tùy chỉnh" + "Mặc định" + "Thông báo" + "Tin nhắn được ghim" + "Vai trò và quyền hạn" + "Tên" + "Bảo mật" + "Chia sẻ phòng" + "Chủ đề" + "Đang cập nhật thông tin…" + "Hiện không có người dùng nào bị cấm." + + "%1$d người" + + "Cấm người dùng" + "Chỉ xóa thành viên" + "Bỏ cấm" + "Họ có thể tham gia lại phòng này nếu được mời." + "Bị cấm" + "Thành viên" + "Quản trị viên" + "Người điều hành" + "Thành viên phòng" + "Đang gỡ cấm %1$s" + "Cho phép tùy chỉnh cài đặt" + "Kích hoạt sẽ thay thế cài đặt mặc định" + "Thông báo cho tôi trong cuộc trò chuyện này khi" + "Bạn có thể thay đổi trong %1$s của mình." + "cài đặt chung" + "Cài đặt mặc định" + "Xóa cài đặt tùy chỉnh" + "Đã xảy ra lỗi khi tải cài đặt thông báo." + "Không thể khôi phục chế độ mặc định, vui lòng thử lại." + "Không thể thiết lập chế độ, hãy thử lại nhé." + "Máy chủ không hỗ trợ tùy chọn này trong phòng mã hóa, bạn sẽ không nhận thông báo ở đây." + "Tất cả tin nhắn." + "Chỉ đề cập và từ khóa" + "Trong phòng này, thông báo cho tôi khi" + "Quản trị viên" + "Thay đổi vai trò của tôi" + "Hạ cấp xuống thành thành viên" + "Hạ cấp xuống làm người điều hành" + "Quản lý thành viên" + "Tin nhắn và nội dung." + "Người điều hành" + "Đặt lại quyền truy cập" + "Sau khi bạn đặt lại quyền truy cập, bạn sẽ mất các cài đặt hiện tại." + "Đặt lại quyền truy cập?" + "Vai trò" + "Chi tiết phòng" + "Vai trò và quyền hạn" + "Sau khi được kích hoạt, mã hóa cho một phòng chat không thể tắt được. Lịch sử tin nhắn chỉ hiển thị cho các thành viên phòng chat kể từ khi họ được mời hoặc kể từ khi họ tham gia phòng chat. +Không ai ngoài các thành viên phòng chat có thể đọc tin nhắn. Điều này có thể ngăn chặn bot và các thiết bị kết nối hoạt động đúng cách. +Chúng tôi không khuyến khích bật mã hóa cho các phòng chat mà bất kỳ ai cũng có thể tìm thấy và tham gia." + "Mã hóa" + "Thành viên không gian" + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + diff --git a/features/roomdetails/impl/src/main/res/values-zh/translations.xml b/features/roomdetails/impl/src/main/res/values-zh/translations.xml index 4101f67260..d803f01fb7 100644 --- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml @@ -23,7 +23,7 @@ "更改聊天室主题" "发送消息" "编辑管理员" - "您将无法撤消此操作。您正在提升用户的权限,使其拥有与您平权。" + "您将无法撤消此操作。您正在提升用户的权限到与您相同的级别。" "添加管理员?" "此操作无法撤销。您正在将所有权转移给所选用户。一旦离开此界面,该操作将永久生效。" "转让所有权" @@ -42,8 +42,8 @@ "您有未保存的更改。" "保存更改?" "添加主题" - "加密的" - "未加密的" + "已加密" + "未加密" "公共聊天室" "编辑详情" "出现未知错误,无法更改信息。" @@ -86,7 +86,7 @@ "仅移除成员" "取消封禁" "如果受到邀请,他们可以重新加入聊天室。" - "从房间取消解封" + "解封用户" "已封禁用户" "成员" @@ -97,7 +97,7 @@ "协管员" "所有者" "聊天室成员" - "解除封禁 %1$s" + "正在解除封禁 %1$s" "允许自定义设置" "开启此功能将覆盖您的默认设置" "在此聊天中通知我以下内容" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt index 5042f942b6..bcf25b2aac 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt @@ -69,6 +69,7 @@ class DefaultRoomDetailsEntryPointTest { } val callback = object : RoomDetailsEntryPoint.Callback { override fun navigateToGlobalNotificationSettings() = lambdaError() + override fun navigateToDeveloperSettings() = lambdaError() override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() diff --git a/features/roomdetailsedit/impl/src/main/res/values-ja/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5dbdbe22e1 --- /dev/null +++ b/features/roomdetailsedit/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "詳細を編集" + "不明な問題が発生したため、情報の更新に失敗しました。" + "ルームを更新することができません" + "詳細を更新中…" + diff --git a/features/roomdetailsedit/impl/src/main/res/values-vi/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..9a5d8948e4 --- /dev/null +++ b/features/roomdetailsedit/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Chỉnh sửa thông tin" + "Có lỗi không xác định, thông tin không được cập nhật." + "Không thể cập nhật phòng" + "Đang cập nhật thông tin…" + diff --git a/features/roomdirectory/impl/src/main/res/values-ja/translations.xml b/features/roomdirectory/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f533dc8eab --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "読み込みに失敗しました" + "ルーム階層" + diff --git a/features/roomdirectory/impl/src/main/res/values-vi/translations.xml b/features/roomdirectory/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..efbe223f79 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Không tải được" + "Danh sách phòng" + diff --git a/features/roommembermoderation/impl/src/main/res/values-ja/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5ca793fd5a --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,22 @@ + + + "ユーザーを追放" + "追放" + "招待されても再度参加することはできません。" + "このメンバーを本当に追放しますか?" + "招待されても再度参加することはできませんが、すべてのルームとスペースにおける権限を維持します。" + "%1$s を追放中" + "削除" + "招待を受け取ると再度参加できます。" + "このメンバーを本当に削除しますか?" + "すべてのルームとスペースにおける権限を維持し、招待によって再度参加することができます。" + "プロフィールを表示" + "ユーザーを削除" + "メンバーを削除し、今後の参加を禁止しますか?" + "%1$s を削除中…" + "ユーザーの追放を解除" + "追放を解除" + "招待によって再度参加することができます。" + "このメンバーの追放を本当に解除しますか?" + "%1$s の追放を解除中" + diff --git a/features/roommembermoderation/impl/src/main/res/values-vi/translations.xml b/features/roommembermoderation/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..07d712cd4c --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,15 @@ + + + "Cấm người dùng" + "Cấm" + "Họ sẽ không thể tham gia lại ngay cả khi được mời." + "Xác nhận cấm thành viên này?" + "Đang cấm %1$s" + "Họ có thể tham gia lại phòng này nếu được mời." + "Xem hồ sơ" + "Xóa người dùng" + "Xóa thành viên và cấm tham gia trong tương lai?" + "Đang xóa %1$s…" + "Bỏ cấm" + "Đang gỡ cấm %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml index aa8b2e3df8..2c18ee4216 100644 --- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -5,7 +5,7 @@ "即使受到邀请,他们也无法再次加入聊天室。" "您确定要封禁该成员吗?" "即使再次受邀,他们也无法加入这个空间,但他们仍将保留其在任何房间或子空间的成员资格。" - "封禁 %1$s" + "正在封禁 %1$s" "移除" "如果受到邀请,他们可以重新加入聊天室。" "您确定要移除此成员吗?" @@ -14,9 +14,9 @@ "移除用户" "删除成员并禁止重新加入?" "正在移除 %1$s……" - "从房间取消解封" + "解封用户" "取消封禁" "如果再次收到邀请,他们可以重新加入该聊天室" "确定要解除该成员的封禁吗?" - "解除封禁 %1$s" + "正在解除封禁 %1$s" diff --git a/features/securebackup/impl/src/main/res/values-cs/translations.xml b/features/securebackup/impl/src/main/res/values-cs/translations.xml index 3669da34ee..171900cede 100644 --- a/features/securebackup/impl/src/main/res/values-cs/translations.xml +++ b/features/securebackup/impl/src/main/res/values-cs/translations.xml @@ -2,16 +2,17 @@ "Vypnout zálohování" "Zapnout zálohování" - "Bezpečně uložte svou kryptografickou identitu a klíče zpráv na serveru. To vám umožní zobrazit historii zpráv na všech nových zařízeních. %1$s." + "To vám umožní zobrazit historii chatu na všech nových zařízeních a je to nutné pro zálohování chatů a digitální identity. %1$s ." "Úložiště klíčů" - "Pro nastavení obnovení musí být zapnuto úložiště klíčů." + "Pro zálohování chatů musí být zapnuto ukládání klíčů." "Nahrát klíče z tohoto zařízení" "Povolit ukládání klíčů" "Změnit klíč pro obnovení" - "Obnovte svou kryptografickou identitu a historii zpráv pomocí klíče pro obnovení, pokud jste ztratili všechna stávající zařízení." + "Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení." "Zadejte klíč pro obnovení" "Vaše úložiště klíčů je momentálně nesynchronizované." - "Nastavení obnovy" + "Získat klíč pro obnovení" + "Vaše chaty jsou automaticky zálohovány pomocí koncového šifrování. Chcete-li tuto zálohu obnovit a zachovat si svou digitální identitu v případě, že ztratíte přístup ke všem svým zařízením, budete potřebovat svůj klíč pro obnovení." "Otevřít %1$s na stolním počítači" "Znovu se přihlaste ke svému účtu" "Když budete vyzváni k ověření vašeho zařízení, vyberte %1$s" @@ -23,12 +24,12 @@ "Podrobnosti o vašem účtu, kontaktech, preferencích a seznamu chatu budou zachovány" "Ztratíte svou stávající historii zpráv" "Budete muset znovu ověřit všechna stávající zařízení a kontakty" - "Obnovte svou identitu pouze v případě, že nemáte přístup k jinému přihlášenému zařízení a ztratili jste klíč pro obnovení." - "Obnovte svou identitu v případě, že nemůžete potvrdit jiným způsobem" - "Vypnout" - "Pokud se odhlásíte ze všech zařízení, přijdete o zašifrované zprávy." - "Opravdu chcete vypnout zálohování?" - "Vypnutím zálohování odstraníte zálohu aktuálního šifrovacího klíče a vypnete další bezpečnostní funkce. V tomto případě budete:" + "Digitální identitu resetujte pouze v případě, že nemáte přístup k jinému ověřenému zařízení a nemáte klíč pro obnovení." + "Nelze potvrdit? Budete muset resetovat svou digitální identitu." + "Smazat" + "Pokud odeberete všechna zařízení, ztratíte svou šifrovanou historii chatu a budete muset resetovat svou digitální identitu." + "Opravdu chcete smazat úložiště klíčů?" + "Smazáním úložiště klíčů odstraníte ze serveru klíče digitální identity a zpráv a vypnete následující bezpečnostní funkce:" "Nemít v nových zařízeních šifrovanou historii zpráv" "Ztratíte přístup k šifrovaným zprávám, pokud jste všude odhlášeni z %1$s" "Opravdu chcete vypnout zálohování?" @@ -58,12 +59,12 @@ "Vygenerovat klíč pro obnovení" "Toto s nikým nesdílejte!" "Nastavení obnovení bylo úspěšné" - "Nastavení obnovy" + "Získat klíč pro obnovení" "Ano, resetovat nyní" "Tento proces je nevratný." - "Opravdu chcete obnovit svou identitu?" + "Opravdu chcete resetovat svou digitální identitu?" "Došlo k neznámé chybě. Zkontrolujte, zda je heslo k účtu správné a zkuste to znovu." "Zadejte…" - "Potvrďte, že chcete obnovit svou identitu." + "Potvrďte, že chcete resetovat svou digitální identitu." "Pro pokračování zadejte heslo k účtu" diff --git a/features/securebackup/impl/src/main/res/values-da/translations.xml b/features/securebackup/impl/src/main/res/values-da/translations.xml index 93b949bfb6..941b7fc480 100644 --- a/features/securebackup/impl/src/main/res/values-da/translations.xml +++ b/features/securebackup/impl/src/main/res/values-da/translations.xml @@ -2,16 +2,17 @@ "Slet nøglelager" "Aktivér sikkerhedskopiering" - "Gem din kryptografiske identitet og meddelelsesnøgler sikkert på serveren. Dette giver dig mulighed for at se din meddelelseshistorik på alle nye enheder. %1$s." + "Dette giver dig mulighed for at se din chathistorik på alle nye enheder og er påkrævet til sikkerhedskopiering af chats og digital identitet.%1$s ." "Nøgleopbevaring" - "Nøglelagring skal være slået til for at konfigurere gendannelse." + "Nøglelagring skal være slået til for at konfigurere gendannelse af dine samtaler." "Upload nøgler fra denne enhed" "Tillad lagring af nøgler" "Skift gendannelsesnøgle" - "Gendan din kryptografiske identitet og beskedhistorik med en gendannelsesnøgle, hvis du har mistet alle dine eksisterende enheder." + "Dine samtaler sikkerhedskopieres automatisk med end-to-end-kryptering. For at kunne gendanne denne sikkerhedskopi og bevare din digitale identitet, hvis du mister adgang til alle dine enheder, får du brug for din gendannelsesnøgle." "Indtast gendannelsesnøgle" "Din nøglelagring er i øjeblikket ikke synkroniseret." - "Opsæt gendannelse" + "Hent gendannelsesnøgle" + "Dine chats sikkerhedskopieres automatisk med end-to-end-kryptering. For at kunne gendanne denne sikkerhedskopi og bevare din digitale identitet, hvis du mister adgang til alle dine enheder, får du brug for din gendannelsesnøgle." "Åbn %1$s på en stationær enhed" "Log ind på din konto igen" "Når du bliver bedt om at verificere din enhed, skal du vælge %1$s" @@ -23,12 +24,12 @@ "Dine kontodetaljer, kontakter, personlige indstilliger og samtaler vil blive gemt" "Du mister al beskedhistorik, der kun er gemt på serveren." "Du bliver nødt til at verificere alle dine eksisterende enheder og kontakter påny" - "Nulstil kun din identitet, hvis du ikke har adgang til en anden enhed, der er logget ind, og du har mistet din gendannelsesnøgle." - "Kan du ikke bekræfte? Du skal nulstille din identitet." - "Slå fra" - "Du mister dine krypterede meddelelser, hvis du er logget ud af alle enheder." - "Er du sikker på, at du vil slå sikkerhedskopiering fra?" - "Hvis du sletter nøglelageret, fjernes din kryptografiske identitet og meddelelsesnøgler fra serveren og følgende sikkerhedsfunktioner deaktiveres:" + "Nulstil kun din digitale identitet, hvis du ikke har adgang til en anden enhed, der er logget ind, og du har mistet din gendannelsesnøgle." + "Kan du ikke bekræfte? Du er nødt til at nulstille din digitale identitet." + "Slet" + "Du mister din krypterede chathistorik og skal nulstille din digitale identitet, hvis du fjerner alle dine enheder." + "Er du sikker på, at du vil slette nøglelageret?" + "Hvis du sletter nøglelageret, fjernes din kryptografiske identitet og beskednøgler fra serveren og følgende sikkerhedsfunktioner deaktiveres:" "Du vil ikke kunne se historikken for krypterede beskeder på nye enheder" "Du mister adgangen til dine krypterede meddelelser, hvis du er logget ud %1$s overalt" "Er du sikker på, at du vil deaktivere nøglelagring og slette lageret?" @@ -58,12 +59,12 @@ "Generer din gendannelsesnøgle" "Del ikke dette med nogen!" "Opsætning af gendannelse lykkedes" - "Opsæt gendannelse" + "Hent gendannelsesnøgle" "Ja, nulstil nu" "Denne proces er irreversibel." - "Er du sikker på, at du ønsker at nulstille din identitet?" + "Er du sikker på, at du vil nulstille din digitale identitet?" "Der opstod en ukendt fejl. Kontroller, at adgangskoden til din konto er korrekt, og prøv igen." "Indtast…" - "Bekræft, at du ønsker at nulstille din identitet." + "Bekræft, at du ønsker at nulstille din digitale identitet." "Indtast adgangskoden til din konto for at fortsætte" diff --git a/features/securebackup/impl/src/main/res/values-fi/translations.xml b/features/securebackup/impl/src/main/res/values-fi/translations.xml index f7d09bb6ca..7cbe0f1af6 100644 --- a/features/securebackup/impl/src/main/res/values-fi/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fi/translations.xml @@ -12,6 +12,7 @@ "Anna palautusavain" "Avainten säilytys ei ole tällä hetkellä synkronoitu." "Hanki palautusavain" + "Keskustelusi varmuuskopioidaan automaattisesti päästä päähän -salauksella. Jotta voit palauttaa tämän varmuuskopion ja säilyttää digitaalisen identiteettisi, kun menetät pääsyn kaikkiin laitteisiisi, tarvitset palautusavaimesi." "Avaa %1$s tietokoneella" "Kirjaudu tilillesi uudelleen" "Kun sinua pyydetään vahvistamaan laitteesi, valitse %1$s" diff --git a/features/securebackup/impl/src/main/res/values-hu/translations.xml b/features/securebackup/impl/src/main/res/values-hu/translations.xml index cf7ab1cb0c..a5d910e76b 100644 --- a/features/securebackup/impl/src/main/res/values-hu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hu/translations.xml @@ -12,6 +12,7 @@ "Adja meg a helyreállítási kulcsot" "A kulcstároló jelenleg nincs szinkronizálva." "Helyreállítási kulcs beszerzése" + "A csevegésekről automatikusan készül biztonsági mentés végpontok közötti titkosítással. A biztonsági mentés helyreállításához és digitális személyazonossága megőrzéséhez szüksége lesz a helyreállítási kulcsára, ha elveszíti a hozzáférést az összes eszközéhez." "Nyissa meg az %1$set egy asztali eszközön" "Jelentkezzen be újra a fiókjába" "Amikor az eszköz ellenőrzését kéri, válassza ezt a lehetőséget: %1$s" @@ -23,11 +24,11 @@ "A fiókadatok, a kapcsolatok, a beállítások és a csevegéslista megmarad" "Elveszíti meglévő üzenetelőzményeit" "Újból ellenőriznie kell az összes meglévő eszközét és csevegőpartnerét" - "Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá másik bejelentkezett eszközhöz, és elvesztette a helyreállítási kulcsot." + "Csak akkor állítsa alaphelyzetbe a személyazonosságát, ha nem fér hozzá más bejelentkezett eszközhöz, és elveszítette a helyreállítási kulcsát." "Nem tudja megerősíteni? Alaphelyzetbe kell állítania a digitális személyazonosságát." - "Kikapcsolás" - "Ha kijelentkezik az összes eszközéről, akkor elveszti a titkosított üzeneteit." - "Biztos, hogy kikapcsolja a biztonsági mentéseket?" + "Törlés" + "Ha eltávolítja az összes eszközét, elveszíti titkosított csevegési előzményeit, és újra be kell állítania digitális személyazonosságát." + "Biztosan törölni szeretné a kulcstárolót?" "A kulcstároló törlése eltávolítja a digitális személyazonosságát és az üzenetkulcsait a kiszolgálóról, és kikapcsolja a következő biztonsági funkciókat:" "Nem lesznek meg a titkosított üzenetek előzményei az új eszközein" "Elveszti a hozzáférését a titkosított üzeneteihez, ha mindenhol kilép az %1$sből" diff --git a/features/securebackup/impl/src/main/res/values-it/translations.xml b/features/securebackup/impl/src/main/res/values-it/translations.xml index fe7ba13c60..188de2c3df 100644 --- a/features/securebackup/impl/src/main/res/values-it/translations.xml +++ b/features/securebackup/impl/src/main/res/values-it/translations.xml @@ -2,16 +2,17 @@ "Disattiva il backup" "Attiva il backup" - "Archivia la tua identità crittografica e le chiavi dei messaggi in modo sicuro sul server. Ciò ti consentirà di visualizzare la cronologia dei messaggi su tutti i nuovi dispositivi. %1$s." + "Questo ti permetterà di visualizzare la cronologia delle conversazioni su qualsiasi nuovo dispositivo ed è necessario per il backup delle chat e dell\'identità digitale.%1$s ." "Archiviazione chiavi" - "L\'archiviazione delle chiavi deve essere attivata per configurare il ripristino." + "Per eseguire il backup delle tue conversazioni, devi attivare l\'archiviazione delle chiavi." "Carica le chiavi da questo dispositivo" "Consenti l\'archiviazione delle chiavi" "Cambia la chiave di recupero" - "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i dispositivi esistenti." + "Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero." "Inserisci la chiave di recupero" "L\'archiviazione delle chiavi non è sincronizzata." - "Configura il recupero" + "Ottieni la chiave di recupero" + "Le tue conversazioni vengono automaticamente salvate con crittografia end-to-end. Per ripristinare questo backup e conservare la tua identità digitale quando perdi l\'accesso a tutti i tuoi dispositivi, avrai bisogno della tua chiave di recupero." "Apri %1$s in un dispositivo desktop" "Accedi nuovamente al tuo account" "Quando ti viene chiesto di verificare il tuo dispositivo, seleziona %1$s" @@ -23,12 +24,12 @@ "I dettagli del tuo account, i contatti, le preferenze e l\'elenco delle conversazioni verranno conservati" "Perderai la cronologia dei messaggi esistente" "Dovrai verificare nuovamente tutti i dispositivi e i contatti esistenti" - "Reimposta la tua identità solo se non hai accesso a un altro dispositivo su cui hai effettuato l\'accesso e hai perso la chiave di recupero." - "Reimposta la tua identità nel caso in cui non riesci a confermare in un altro modo" - "Disattiva" - "Perderai i tuoi messaggi cifrati se sei disconnesso da tutti i dispositivi." - "Vuoi davvero disattivare il backup?" - "La disattivazione del backup rimuoverà il backup dell\'attuale chiave crittografica e disattiverà altre funzioni di sicurezza. In questo caso:" + "Reimposta la tua identità digitale solo se non hai accesso a un altro dispositivo verificato e non disponi della tua chiave di recupero." + "Non riesci a confermare? Dovrai reimpostare la tua identità digitale." + "Elimina" + "Se rimuovi tutti i tuoi dispositivi, perderai la cronologia delle conversazioni cifrate e dovrai reimpostare la tua identità digitale." + "Sei sicuro di voler eliminare l\'archivio delle chiavi?" + "L\'eliminazione della memoria delle chiavi rimuoverà l\'identità digitale e le chiavi dei messaggi dal server e disattiverà le seguenti funzionalità di sicurezza:" "Non avrai la cronologia dei messaggi cifrati su nuovi dispositivi" "Perderai l\'accesso ai tuoi messaggi cifrati se ti sei disconnesso da %1$s ovunque" "Vuoi davvero disattivare il backup?" @@ -58,12 +59,12 @@ "Genera la tua chiave di recupero" "Non condividerla con nessuno!" "Configurazione del recupero completata" - "Configura il recupero" + "Ottieni la chiave di recupero" "Sì, reimposta ora" "Questo processo è irreversibile." - "Sei sicuro di voler reimpostare la crittografia?" + "Sei sicuro di voler reimpostare la tua identità digitale?" "Si è verificato un errore sconosciuto. Controlla che la password del tuo account sia corretta e riprova." "Inserisci…" - "Conferma di voler reimpostare la crittografia." + "Conferma che desideri reimpostare la tua identità digitale." "Inserisci la password del tuo account per continuare" diff --git a/features/securebackup/impl/src/main/res/values-ja/translations.xml b/features/securebackup/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..7a7dd041fe --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,70 @@ + + + "保存されている鍵を削除" + "バックアップを有効化" + "暗号化されたデジタルIDとメッセージの鍵をサーバーに保存します。これにより、新しい端末から過去のメッセージを確認することができます。%1$s" + "鍵の保管庫" + "チャットをバックアップするには、鍵の保管庫を使用する必要があります。" + "この端末上の鍵をアップロードします" + "鍵の保管庫を使用します" + "回復鍵を変更" + "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" + "回復鍵を入力" + "鍵の保管庫を現在同期できません。" + "回復鍵を作成" + "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" + "%1$s をコンピュータで開く" + "再度サインインしてください" + "端末の認証を要求されたら %1$s を選択してください" + "\"すべてリセット\"" + "指示に従って回復鍵を作成してください" + "生成された回復鍵をパスワードマネージャや暗号化に対応するメモアプリに保存してください。" + "他の端末を使用して暗号化をリセット" + "リセットを続行" + "アカウントの情報と連絡先や設定などは残ります" + "サーバー上にのみ存在する過去のメッセージは確認できなくなります" + "すべての端末と連絡先を再度検証する必要があります" + "デジタルIDのリセットは、他のサインイン済みの端末と、回復鍵の両方へのアクセスを失った場合にのみ行ってください。" + "認証できませんか?デジタルIDをリセットする必要があります。" + "削除" + "すべての端末を削除してしまうと、暗号化された会話が失われ、デジタルIDをリセットする必要があります。" + "本当に鍵の保管庫を削除しますか?" + "鍵の保管庫を消去することにより、デジタルIDとメッセージの鍵はサーバーから削除され、次のセキュリティ機能が無効化されます:" + "新しい端末で暗号化された過去のメッセージを確認できなくなります" + "すべての端末で %1$s からサインアウトすると、暗号化されたメッセージを確認することはできなくなります。" + "本当に鍵を保管庫から削除しますか?" + "既存の回復鍵を紛失した場合は、新しい回復鍵を生成してください。回復鍵を更新すると、それ以前の回復鍵は使用できなくなります。" + "新しい回復鍵を生成する" + "誰にも共有しないでください!" + "回復鍵を更新しました" + "回復鍵を変更しますか?" + "新しい回復鍵を生成" + "誰にもこの画面を見せないでください!" + "鍵の保管庫にアクセスするには、もう一度お試しください。" + "回復鍵が間違っています" + "代わりにセキュリティキーまたはセキュリティフレーズを入力することも可能です。" + "回復鍵を入力してください…" + "回復鍵を紛失しましたか?" + "回復鍵が承認されました" + "回復鍵を入力してください" + "回復鍵をコピーしました" + "生成中…" + "回復鍵を保存" + "この回復鍵をパスワードマネージャーや、暗号化に対応するメモアプリなどに記録するか、物理的な金庫などに書き留めて保管してください。" + "タップして回復鍵をコピー" + "回復鍵を安全な場所に保管してください" + "後からこの回復鍵を確認することはできません。" + "回復鍵を保存しましたか?" + "鍵の保管庫は回復鍵によって保護されています。新しい回復鍵が必要な場合は「回復鍵を変更」を選択して再生成できます。" + "新しい回復鍵を生成する" + "誰にも共有しないでください!" + "回復鍵の設定に成功しました" + "回復鍵を作成" + "はい、リセットします" + "この操作は元に戻せません。" + "本当にデジタルIDをリセットしますか?" + "不明な問題が発生しました。アカウントのパスワードが正しいことを確認してもう一度試してください。" + "回復鍵を入力してください…" + "デジタルIDをリセットしようとしています。" + "アカウントのパスワードを入力" + diff --git a/features/securebackup/impl/src/main/res/values-ko/translations.xml b/features/securebackup/impl/src/main/res/values-ko/translations.xml index d2aec9c27d..182ae8d59a 100644 --- a/features/securebackup/impl/src/main/res/values-ko/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ko/translations.xml @@ -4,14 +4,15 @@ "백업 활성화" "이 설정을 통해 새로운 기기에서도 대화 기록을 확인할 수 있으며, 대화 및 디지털 신원 백업을 위해 반드시 필요합니다. %1$s." "키 저장소" - "복구 설정을 하려면 키 저장을 켜야 합니다." + "대화 내용을 백업하려면 키 저장소를 켜야 합니다." "이 장치에서 키 업로드" "키 저장 허용" "복구 키 변경" - "기존의 모든 기기를 분실한 경우, 복구 키를 사용하여 암호화 ID와 메시지 기록을 복구할 수 있습니다." + "대화 내용은 종단간 암호화 기술로 자동 백업됩니다. 모든 기기를 사용할 수 없는 상황에서 백업을 복구하고 디지털 신원을 유지하려면 복구 키가 반드시 필요합니다." "복구 키를 입력하세요" "현재 키 저장소가 동기화되지 않았습니다." "복구 키 가져오기" + "대화 내용은 종단간 암호화로 자동 백업됩니다. 모든 기기를 사용할 수 없는 상황에서 백업을 복구하고 디지털 신원을 유지하려면 복구 키가 반드시 필요합니다." "데스크톱 장치에서 %1$s 을 엽니다." "계정에 다시 로그인하세요" "장치를 확인하라는 메시지가 표시되면, %1$s 을 선택하세요" diff --git a/features/securebackup/impl/src/main/res/values-ru/translations.xml b/features/securebackup/impl/src/main/res/values-ru/translations.xml index 4ae9a2e423..ec5237235e 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -12,6 +12,7 @@ "Введите ключ восстановления" "В настоящее время резервная копия ваших чатов не синхронизирована." "Получить ключ восстановления" + "Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления." "Откройте %1$s на компьютере" "Войдите в свой аккаунт еще раз" "Когда потребуется подтвердить устройство, выберите %1$s" @@ -23,12 +24,12 @@ "Данные вашего аккаунта, контакты, настройки и список чатов будут сохранены" "Вы потеряете историю тех сообщений, которые хранятся только на сервере" "Вам нужно будет заново подтвердить все существующие устройства и контакты." - "Сбрасывайте личность только в том случае, если у вас нет доступа к другим устройству, на которых выполнен вход, и вы потеряли ключ восстановления." - "Не можете подтвердить? Вам потребуется сбросить личность вашей учетной записи." - "Выключить" + "Сбрасывайте ключ шифрования только в том случае, если у вас нет доступа к другому устройству, на котором выполнен вход, и вы потеряли ключ восстановления." + "Не можете подтвердить? Вам потребуется сбросить свою цифровую идентификацию." + "Удалить" "Вы потеряете зашифрованные сообщения, если выйдете из всех устройств." - "Вы действительно хотите отключить резервное копирование?" - "Удаление хранилища ключей приведёт к удалению вашей криптографической личности и ключей сообщений с сервера, а также отключению следующих функций безопасности:" + "Вы уверены, что хотите удалить хранилище ключей?" + "Удаление хранилища ключей приведет к удалению вашей цифровой идентификации и ключей сообщений с сервера, а также к отключению следующих функций безопасности:" "Нет зашифрованной истории сообщений на новых устройствах" "Вы потеряете доступ к зашифрованным сообщениям, если выйдете из %1$s везде" "Вы уверены, что хотите отключить хранение ключей и удалить их?" diff --git a/features/securebackup/impl/src/main/res/values-vi/translations.xml b/features/securebackup/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..70913a5e24 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,44 @@ + + + "Xóa kho lưu trữ khóa" + "Bật tính năng sao lưu" + "Điều này cho phép bạn xem lịch sử trò chuyện trên bất kỳ thiết bị mới nào và cần thiết để sao lưu trò chuyện cũng như danh tính kỹ thuật số. %1$s." + "Lưu trữ khóa" + "Thay đổi khóa khôi phục." + "Nhập mã khôi phục." + "Kho lưu trữ khóa của bạn hiện đang không đồng bộ." + "Lấy khóa khôi phục." + "Xoá" + "Bạn sẽ mất các tin nhắn đã mã hóa nếu bạn đăng xuất khỏi tất cả các thiết bị." + "Bạn có chắc muốn xóa lưu trữ khóa không?" + "Xóa lưu trữ khóa sẽ loại bỏ danh tính kỹ thuật số và khóa tin nhắn của bạn khỏi máy chủ, đồng thời tắt các tính năng bảo mật sau:" + "Lịch sử tin nhắn mã hóa sẽ không có trên thiết bị mới." + "Bạn sẽ mất quyền truy cập vào các tin nhắn được mã hóa nếu đăng xuất khỏi %1$s trên tất cả thiết bị" + "Bạn có chắc muốn tắt lưu trữ khóa và xóa nó không?" + "Nếu mất khóa khôi phục hiện tại, hãy tạo khóa mới. Khóa cũ sẽ không còn dùng được sau khi thay đổi." + "Tạo khóa khôi phục mới." + "Đừng chia sẻ điều này với bất kỳ ai!" + "Khóa khôi phục đã thay đổi." + "Thay đổi khóa khôi phục?" + "Đảm bảo không ai có thể nhìn thấy màn hình này!" + "Thử lại để xác nhận quyền truy cập lưu trữ khóa." + "Khóa khôi phục không chính xác." + "Nếu bạn có khóa bảo mật hoặc mật khẩu, bạn cũng có thể dùng." + "Nhập…" + "Khóa khôi phục xác nhận thành công." + "Nhập khóa khôi phục của bạn." + "Đã sao chép khóa khôi phục." + "Đang tạo…" + "Lưu khóa khôi phục." + "Ghi khóa khôi phục vào nơi an toàn, như trình quản lý mật khẩu, ghi chú mã hóa hoặc két sắt." + "Chạm để sao chép khóa khôi phục." + "Hãy lưu khóa khôi phục ở nơi an toàn." + "Sau bước này, bạn sẽ không còn truy cập khóa khôi phục mới." + "Bạn đã lưu lại khóa khôi phục chưa?" + "Lưu trữ khóa của bạn được bảo vệ bằng một khóa khôi phục. Nếu cần một khóa khôi phục mới sau khi thiết lập, bạn có thể tạo lại bằng cách chọn \'Thay đổi khóa khôi phục\'." + "Tạo khóa khôi phục của bạn." + "Đừng chia sẻ điều này với bất kỳ ai!" + "Thiết lập khôi phục thành công" + "Lấy khóa khôi phục." + "Nhập…" + diff --git a/features/securebackup/impl/src/main/res/values-zh/translations.xml b/features/securebackup/impl/src/main/res/values-zh/translations.xml index 438a05f893..8d86e496cb 100644 --- a/features/securebackup/impl/src/main/res/values-zh/translations.xml +++ b/features/securebackup/impl/src/main/res/values-zh/translations.xml @@ -11,7 +11,7 @@ "如果您丢失了所有现有设备,使用恢复密钥恢复您的密码学身份和消息历史记录。" "输入恢复密钥" "您的密钥存储当前不同步。" - "设置恢复" + "获取恢复密钥" "在桌面设备中打开 %1$s" "再次登录您的账户" "当要求验证您的设备时,选择 %1$s" @@ -23,8 +23,8 @@ "您的账户信息、联系人、偏好设置和聊天列表将被保留" "您将丢失现有的消息历史记录" "您将需要再次验证所有您的现有设备和联系人" - "仅当您无法访问其他已登录设备并且丢失了恢复密钥时才重置您的身份。" - "如果您无法通过其他方式确认,请重置您的身份" + "仅当您无法访问其他已登录设备并且丢失了恢复密钥时才重置您的数字身份。" + "无法确认?那么你需要重置您的数字身份。" "关闭" "如果您登出所有设备,您的加密消息将丢失。" "您确定要关闭备份吗?" @@ -34,7 +34,7 @@ "您确定要关闭备份吗?" "如果您丢失了现有的恢复密钥,请获取新的恢复密钥。更改恢复密钥后,您的旧密钥将不再起作用。" "生成新的恢复密钥" - "不要告诉任何人!" + "请勿与任何人分享!" "恢复密钥已更改" "更改恢复密钥?" "创建新的恢复密钥" @@ -56,14 +56,14 @@ "您保存了恢复密钥吗?" "您的聊天备份受恢复密钥保护。如果您在安装后需要新的恢复密钥,则可以通过选择「更改恢复密钥」来重新创建。" "生成恢复密钥" - "不要告诉任何人!" + "请勿与任何人分享!" "恢复设置成功" - "设置恢复" + "获取恢复密钥" "是的,立即重置" "此过程不可逆。" - "您确定要重置加密吗?" + "您确定要重置您的数字身份吗?" "发生未知错误。请检查您的帐户密码是否正确,然后重试。" "输入……" - "确认您要重置加密。" + "确认您要重置您的数字身份。" "输入您的账户密码以继续" diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt index 9230c1183a..48ab41a820 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenter.kt @@ -78,9 +78,6 @@ class SecurityAndPrivacyPresenter( val isKnockEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) }.collectAsState(false) - val isSpaceSettingsEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) - }.collectAsState(false) val saveAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val homeserverName = remember { matrixClient.userIdServerName() } @@ -248,7 +245,6 @@ class SecurityAndPrivacyPresenter( saveAction = saveAction.value, permissions = permissions, isSpace = roomInfo.isSpace, - isSpaceSettingsEnabled = isSpaceSettingsEnabled, selectableJoinedSpaces = selectableJoinedSpaces, spaceSelectionMode = spaceSelectionMode, eventSink = ::handleEvent, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt index 6ec47ba183..26e77b3c70 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyState.kt @@ -29,7 +29,6 @@ data class SecurityAndPrivacyState( val homeserverName: String, val showEnableEncryptionConfirmation: Boolean, private val isKnockEnabled: Boolean, - private val isSpaceSettingsEnabled: Boolean, val saveAction: AsyncAction, val isSpace: Boolean, private val permissions: SecurityAndPrivacyPermissions, @@ -37,7 +36,7 @@ data class SecurityAndPrivacyState( private val spaceSelectionMode: SpaceSelectionMode, val eventSink: (SecurityAndPrivacyEvent) -> Unit ) { - val isSpaceMemberSelectable = isSpaceSettingsEnabled && spaceSelectionMode != SpaceSelectionMode.None + val isSpaceMemberSelectable = spaceSelectionMode != SpaceSelectionMode.None // Show SpaceMember option in two cases: // - SpaceMember is the current saved value diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt index 95cb45d641..19124302e3 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt @@ -138,7 +138,6 @@ fun aSecurityAndPrivacyState( isSpace: Boolean = false, selectableJoinedSpaces: Set = emptySet(), spaceSelectionMode: SpaceSelectionMode = SpaceSelectionMode.None, - isSpaceSettingsEnabled: Boolean = true, eventSink: (SecurityAndPrivacyEvent) -> Unit = {} ) = SecurityAndPrivacyState( editedSettings = editedSettings, @@ -151,6 +150,5 @@ fun aSecurityAndPrivacyState( isSpace = isSpace, selectableJoinedSpaces = selectableJoinedSpaces.toImmutableSet(), spaceSelectionMode = spaceSelectionMode, - isSpaceSettingsEnabled = isSpaceSettingsEnabled, eventSink = eventSink, ) diff --git a/features/securityandprivacy/impl/src/main/res/values-it/translations.xml b/features/securityandprivacy/impl/src/main/res/values-it/translations.xml index 0cf3326022..41b0c1a047 100644 --- a/features/securityandprivacy/impl/src/main/res/values-it/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-it/translations.xml @@ -2,9 +2,16 @@ "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Modifica indirizzo" + "Spazi in cui i membri possono entrare nella stanza senza invito." + "Gestisci gli spazi" + "(Spazio sconosciuto)" + "Altri spazi di cui non sei membro" + "I tuoi spazi" "Aggiungi indirizzo" + "Chiunque si trovi in spazi autorizzati può partecipare, ma tutti gli altri devono richiedere l\'accesso." "Chiunque deve richiedere l\'accesso." "Chiedi di entrare" + "Chiunque all\'interno di %1$s può partecipare, mentre tutti gli altri devono richiedere l\'accesso." "Sì, attiva la crittografia" "Una volta attivata, la crittografia di una stanza non può essere disattivata, la cronologia dei messaggi sarà visibile solo ai membri della stanza da quando sono stati invitati o da quando sono entrati nella stanza. Nessuno, oltre ai membri della stanza, sarà in grado di leggere i messaggi. Ciò potrebbe impedire ai bot e ai bridge di funzionare correttamente. @@ -15,19 +22,25 @@ Non consigliamo di attivare la crittografia per le stanze che chiunque può trov "Attiva la crittografia end-to-end" "Chiunque può partecipare." "Chiunque" + "Scegli quali membri dello spazio possono accedere a questa stanza senza invito.%1$s" + "Gestisci gli spazi" "Solo le persone invitate possono entrare." "Solo su invito" "Accesso" + "Chiunque si trovi in ​​spazi autorizzati può partecipare." + "Chiunque in %1$s può partecipare." + "Membri dello spazio" "Gli spazi non sono attualmente supportati" "Per renderlo visibile nell\'elenco pubblico, avrai bisogno di un indirizzo." "Indirizzo" "Consenti la ricerca di questa stanza effettuando una ricerca nell\'elenco delle stanze pubbliche di %1$s" "Consenti di essere trovato effettuando una ricerca nell\'elenco pubblico." "Visibile nell\'elenco pubblico" - "Chiunque" + "Chiunque (la cronologia è pubblica)" + "Le modifiche non interesseranno i messaggi passati, ma solo quelli nuovi. %1$s" "Chi può leggere la cronologia messaggi" - "Solo membri da quando sono stati invitati" - "Solo membri da dopo aver selezionato questa opzione" + "Members invited" + "Members (cronologia completa)" "Gli indirizzi delle stanze sono modi per trovare e accedervi. In questo modo puoi anche condividere facilmente la tua stanze con altri. Puoi scegliere di pubblicare la tua stanza nell\'elenco delle stanza pubbliche dell\'homeserver." "Pubblicazione della stanza" diff --git a/features/securityandprivacy/impl/src/main/res/values-ja/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..110d19a325 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,50 @@ + + + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレスの編集" + "招待なしでルームへの参加が可能なスペース" + "スペースを管理" + "(不明なスペース)" + "参加していない他のスペース" + "あなたのスペース" + "アドレスを追加" + "認証済みのスペースに所属するユーザーのみが参加できます。それ以外のユーザーは参加へのリクエストが必要です。" + "参加のリクエストが必須です。" + "参加をリクエスト" + "%1$s に所属するユーザーのみが参加できます。それ以外のユーザーは参加のリクエストが必要です。" + "暗号化を有効にする" + "暗号化が有効のルームを再び無効化することはできません。過去のメッセージの参照は、ユーザーが招待された、あるいは参加した以降に投稿された内容に限定されます。 +ルームのメンバー以外がメッセージを確認することはできないため、bot やブリッジのサービスが正常に動作しない可能性があります。 +公開スペースを暗号化することは一般に推奨されません。" + "暗号化を有効にしますか?" + "一度有効にすると元に戻すことはできません。" + "暗号化" + "エンドツーエンド暗号化を有効にする" + "誰でも参加できます" + "全員" + "招待無しで参加できるユーザーが所属するルームを選択してください。%1$s" + "スペースを管理" + "招待されたユーザーのみ参加できます。" + "招待制" + "アクセス" + "認証済みのスペースに所属するすべてのユーザーが参加できます。" + "%1$s に所属するすべてのユーザーが参加できます。" + "スペースのメンバー" + "スペースは現在対応していません。" + "公開ディレクトリで自分を見つけられるようにするには、アドレスが必要です。" + "アドレス" + "%1$s の公開ルームの検索結果に、このルームを表示します" + "公開ディレクトリの検索結果に表示" + "公開ディレクトリに表示" + "全員(履歴を公開)" + "過去のメッセージに変更は適用されません。新規のメッセージにのみ適用されます。%1$s" + "履歴を表示するユーザー" + "招待済みのユーザー" + "ユーザー (すべての履歴)" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。 +ホームサーバーの公開ディレクトリにルームを表示するかを設定できます。" + "ルームの公開" + "ルームアドレスはルームの検索やアクセスに役立ち、他のユーザーにルームを簡単に共有できます。" + "視認性" + "セキュリティとプライバシー" + diff --git a/features/securityandprivacy/impl/src/main/res/values-vi/translations.xml b/features/securityandprivacy/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c400036307 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,11 @@ + + + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + "Chỉnh sửa địa chỉ" + "Sau khi được kích hoạt, mã hóa cho một phòng chat không thể tắt được. Lịch sử tin nhắn chỉ hiển thị cho các thành viên phòng chat kể từ khi họ được mời hoặc kể từ khi họ tham gia phòng chat. +Không ai ngoài các thành viên phòng chat có thể đọc tin nhắn. Điều này có thể ngăn chặn bot và các thiết bị kết nối hoạt động đúng cách. +Chúng tôi không khuyến khích bật mã hóa cho các phòng chat mà bất kỳ ai cũng có thể tìm thấy và tham gia." + "Mã hóa" + "Thành viên không gian" + "Bạn cần một địa chỉ để hiển thị trong danh bạ công khai." + diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt index d2844c79f0..34b4222053 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyPresenterTest.kt @@ -416,11 +416,6 @@ class SecurityAndPrivacyPresenterTest { val presenter = createSecurityAndPrivacyPresenter( room = room, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.SpaceSettings.key to true, - ) - ) ) presenter.test { skipItems(1) @@ -461,11 +456,6 @@ class SecurityAndPrivacyPresenterTest { room = room, navigator = navigator, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.SpaceSettings.key to true, - ) - ) ) presenter.test { skipItems(1) @@ -587,7 +577,6 @@ class SecurityAndPrivacyPresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.Knock.key to true, - FeatureFlags.SpaceSettings.key to true, ) ) ) @@ -633,7 +622,6 @@ class SecurityAndPrivacyPresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.Knock.key to true, - FeatureFlags.SpaceSettings.key to true, ) ) ) @@ -859,9 +847,6 @@ class SecurityAndPrivacyPresenterTest { val presenter = createSecurityAndPrivacyPresenter( room = room, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SpaceSettings.key to true) - ) ) presenter.test { skipItems(1) @@ -901,9 +886,6 @@ class SecurityAndPrivacyPresenterTest { val presenter = createSecurityAndPrivacyPresenter( room = room, matrixClient = client, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf(FeatureFlags.SpaceSettings.key to true) - ) ) presenter.test { skipItems(1) @@ -975,7 +957,6 @@ class SecurityAndPrivacyPresenterTest { featureFlagService = FakeFeatureFlagService( initialState = mapOf( FeatureFlags.Knock.key to true, - FeatureFlags.SpaceSettings.key to true, ) ) ) diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt index fee1278fce..fd2c8ff194 100644 --- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt @@ -76,7 +76,7 @@ class SharePresenterTest { fun `present - on room selected ok`() = runTest { val joinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline().apply { - sendMessageLambda = { _, _, _ -> Result.success(Unit) } + sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) } }, ) val matrixClient = FakeMatrixClient().apply { @@ -103,7 +103,7 @@ class SharePresenterTest { fun `present - send text ok`() = runTest { val joinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline().apply { - sendMessageLambda = { _, _, _ -> Result.success(Unit) } + sendMessageLambda = { _, _, _, _, _ -> Result.success(Unit) } }, ) val matrixClient = FakeMatrixClient().apply { diff --git a/features/signedout/impl/src/main/res/values-ja/translations.xml b/features/signedout/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..27fc2a1d4b --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,8 @@ + + + "他のセッションでパスワードを変更しました" + "このセッションは他のセッションより削除されました" + "サーバー管理者があなたのアクセスを無効にしました" + "以下のいずれかの理由によってサインアウトされました。%s を引き続き使用するには再度サインインしてください。" + "サインアウトしました" + diff --git a/features/signedout/impl/src/main/res/values-vi/translations.xml b/features/signedout/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..320f4a8731 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,8 @@ + + + "Bạn đã thay đổi mật khẩu trên phiên đăng nhập khác." + "Bạn đã xóa phiên đăng nhập này từ một phiên khác." + "Quản trị viên của máy chủ đã thu hồi quyền truy cập của bạn." + "Bạn có thể đã bị đăng xuất vì một trong những lý do được liệt kê bên dưới. Vui lòng đăng nhập lại để tiếp tục sử dụng %s ." + "Bạn đã đăng xuất." + diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index 807d139e6a..5ce6575493 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -29,8 +29,6 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias @@ -66,7 +64,6 @@ class SpacePresenter( private val joinRoom: JoinRoom, private val acceptDeclineInvitePresenter: Presenter, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, - private val featureFlagService: FeatureFlagService, private val spaceService: SpaceService, ) : Presenter { private var children by mutableStateOf>(persistentListOf()) @@ -99,16 +96,13 @@ class SpacePresenter( val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms -> perms.spacePermissions() } - val isSpaceSettingsEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.SpaceSettings) - }.collectAsState(false) val roomInfo by room.roomInfoFlow.collectAsState() val canAccessSpaceSettings by remember { - derivedStateOf { isSpaceSettingsEnabled && permissions.settingsPermissions.hasAny(roomInfo.joinRule) } + derivedStateOf { permissions.settingsPermissions.hasAny(roomInfo.joinRule) } } val canEditSpaceGraph by remember { - derivedStateOf { isSpaceSettingsEnabled && permissions.canEditSpaceGraph } + derivedStateOf { permissions.canEditSpaceGraph } } val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } diff --git a/features/space/impl/src/main/res/values-be/translations.xml b/features/space/impl/src/main/res/values-be/translations.xml index dbd39abf09..1a3adafd72 100644 --- a/features/space/impl/src/main/res/values-be/translations.xml +++ b/features/space/impl/src/main/res/values-be/translations.xml @@ -1,4 +1,5 @@ + "Пакінуць прастору" "Ролі і дазволы" diff --git a/features/space/impl/src/main/res/values-it/translations.xml b/features/space/impl/src/main/res/values-it/translations.xml index 62f4787002..20859ee70f 100644 --- a/features/space/impl/src/main/res/values-it/translations.xml +++ b/features/space/impl/src/main/res/values-it/translations.xml @@ -8,10 +8,20 @@ "Seleziona le stanze che desideri abbandonare e di cui non sei l\'unico amministratore:" "Prima di poter uscire, devi assegnare un altro amministratore a questo spazio." + "Sei l\'unico proprietario di %1$s. Devi trasferire la proprietà a qualcun altro prima di andartene." "Non verrai rimosso dalle seguenti stanze perché sei l\'unico amministratore:" "Uscire da %1$s?" "Sei l\'unico amministratore di %1$s" + "Trasferisci proprietà" + "Stanza" + "L\'aggiunta di una stanza non influirà sull\'accesso alla stessa. Per modificare l\'accesso, vai su Impostazioni stanza > Sicurezza & privacy." + "Aggiungi la tua prima stanza" "Visualizza membri" + "La rimozione di una stanza non influirà sull\'accesso alla stessa. Per modificare l\'accesso, vai su Informazioni sulla stanza > Sicurezza & privacy" + + "Rimuovi %1$d stanza da %2$s" + "Rimuovi %1$d stanze da %2$s" + "Esci dallo spazio" "Ruoli e autorizzazioni" "Sicurezza e privacy" diff --git a/features/space/impl/src/main/res/values-ja/translations.xml b/features/space/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..e00d045038 --- /dev/null +++ b/features/space/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,26 @@ + + + "所有者を選択" + "%1$s (管理者)" + + "%1$d 個のルームとスペースを退出" + + "あなたが唯一の管理者であるルーム以外を選択してください。" + "このスペースを退出する前に、新しく管理者を設定してください。" + "あなたは %1$s の唯一の所有者です。退出する前に所有権を譲与する必要があります。" + "あなたが唯一の管理者であるため、以下のルームからは退出しません。" + "%1$s を退出しますか?" + "あなたが唯一の %1$s の管理者です。" + "所有権の譲渡" + "ルーム" + "ルームの追加はルームへのアクセスに影響しません。アクセスの設定は、ルームの設定 > セキュリティーとプライバシー から変更できます。" + "最初のルームを追加しましょう" + "メンバーを表示" + "ルームの削除はルームへのアクセスに影響しません。アクセスの設定は、ルームの設定 > セキュリティーとプライバシー から変更できます。" + + "%2$s から%1$d 個のルームを削除" + + "スペースを退出" + "役割と権限" + "セキュリティとプライバシー" + diff --git a/features/space/impl/src/main/res/values-sv/translations.xml b/features/space/impl/src/main/res/values-sv/translations.xml index 794d7db626..f4427c384d 100644 --- a/features/space/impl/src/main/res/values-sv/translations.xml +++ b/features/space/impl/src/main/res/values-sv/translations.xml @@ -1,6 +1,7 @@ "Välj ägare" + "Välj de rum du vill lämna och som du inte är ensam administratör för:" "Lämna %1$s?" "Lämna utrymmet" "Roller och behörigheter" diff --git a/features/space/impl/src/main/res/values-vi/translations.xml b/features/space/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..a19747d029 --- /dev/null +++ b/features/space/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Rời space" + "Vai trò và quyền hạn" + diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 1d38e2e0f7..ba4e10a447 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -11,6 +11,9 @@ package io.element.android.features.space.impl.root import com.google.common.truth.Truth.assertThat +import com.google.testing.junit.testparameterinjector.KotlinTestParameters.namedTestValues +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState @@ -19,8 +22,6 @@ import io.element.android.features.invite.api.toInviteData import io.element.android.features.invite.test.InMemorySeenInvitesStore import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias @@ -51,8 +52,10 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test +import org.junit.runner.RunWith import im.vector.app.features.analytics.plan.JoinedRoom as AnalyticsJoinedRoom +@RunWith(TestParameterInjector::class) class SpacePresenterTest { @Test fun `present - initial state`() = runTest { @@ -75,16 +78,7 @@ class SpacePresenterTest { } @Test - fun `present - canAccessSpaceSettings false when space settings ff is enabled but no permissions`() = runTest { - val presenter = createSpacePresenter(spaceSettingsEnabled = true) - presenter.test { - val state = awaitItem() - assertThat(state.canAccessSpaceSettings).isFalse() - } - } - - @Test - fun `present - canAccessSpaceSettings true when space settings ff is enabled and has permissions`() = runTest { + fun `present - canAccessSpaceSettings true when has permissions`() = runTest { val room = FakeBaseRoom( roomPermissions = FakeRoomPermissions( canSendState = { true } @@ -92,7 +86,6 @@ class SpacePresenterTest { ) val presenter = createSpacePresenter( room = room, - spaceSettingsEnabled = true, ) presenter.test { skipItems(1) @@ -271,21 +264,11 @@ class SpacePresenterTest { } @Test - fun `present - accept invite is transmitted to acceptDeclineInviteState`() { - `invite action is transmitted to acceptDeclineInviteState`( - acceptInvite = true, - ) - } - - @Test - fun `present - decline invite is transmitted to acceptDeclineInviteState`() { - `invite action is transmitted to acceptDeclineInviteState`( - acceptInvite = false, - ) - } - - private fun `invite action is transmitted to acceptDeclineInviteState`( - acceptInvite: Boolean, + fun `present - invite action is transmitted to acceptDeclineInviteState`( + @TestParameter acceptInvite: Boolean = namedTestValues( + "accept" to true, + "decline" to false, + ), ) = runTest { val eventRecorder = EventsRecorder() val anInvitedRoom = aSpaceRoom( @@ -627,7 +610,6 @@ class SpacePresenterTest { lambda = { _, _, _ -> Result.success(Unit) }, ), acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, - spaceSettingsEnabled: Boolean = false, spaceService: FakeSpaceService = FakeSpaceService(), ): SpacePresenter { return SpacePresenter( @@ -638,11 +620,6 @@ class SpacePresenterTest { joinRoom = joinRoom, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, sessionCoroutineScope = this, - featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.SpaceSettings.key to spaceSettingsEnabled, - ) - ), spaceService = spaceService, ) } diff --git a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt index 5bf015c0f0..059002d983 100644 --- a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt +++ b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/ConfirmingStartDmWithMatrixUser.kt @@ -13,4 +13,5 @@ import io.element.android.libraries.matrix.api.user.MatrixUser data class ConfirmingStartDmWithMatrixUser( val matrixUser: MatrixUser, + val isUserIdentityUnknown: Boolean, ) : AsyncAction.Confirming diff --git a/features/startchat/impl/build.gradle.kts b/features/startchat/impl/build.gradle.kts index 6ab1a361e9..805dcc742b 100644 --- a/features/startchat/impl/build.gradle.kts +++ b/features/startchat/impl/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) - implementation(projects.libraries.usersearch.impl) + implementation(projects.libraries.usersearch.api) implementation(projects.services.analytics.api) implementation(libs.coil.compose) implementation(projects.libraries.featureflag.api) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt index a484fe2e72..3bfbd1ca18 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/DefaultStartDMAction.kt @@ -15,6 +15,8 @@ import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.StartDMAction import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.StartDMResult @@ -26,6 +28,7 @@ import io.element.android.services.analytics.api.AnalyticsService class DefaultStartDMAction( private val matrixClient: MatrixClient, private val analyticsService: AnalyticsService, + private val featureFlagService: FeatureFlagService, ) : StartDMAction { override suspend fun execute( matrixUser: MatrixUser, @@ -44,7 +47,11 @@ class DefaultStartDMAction( actionState.value = AsyncAction.Failure(result.throwable) } StartDMResult.DmDoesNotExist -> { - actionState.value = ConfirmingStartDmWithMatrixUser(matrixUser = matrixUser) + val identityState = matrixClient.encryptionService.getUserIdentity(matrixUser.userId, fallbackToServer = false).getOrNull() + actionState.value = ConfirmingStartDmWithMatrixUser( + matrixUser = matrixUser, + isUserIdentityUnknown = featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite) && identityState == null + ) } } } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt index e176f202ad..7afbe19c3d 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt @@ -58,6 +58,8 @@ class StartChatPresenter( featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch) }.collectAsState(initial = false) + val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false) + fun handleEvent(event: StartChatEvents) { when (event) { is StartChatEvents.StartDM -> localCoroutineScope.launch { @@ -76,6 +78,7 @@ class StartChatPresenter( userListState = userListState, startDmAction = startDmActionState.value, isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, + enableKeyShareOnInvite = enableKeyShareOnInvite.value, eventSink = ::handleEvent, ) } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt index 65f977d3e3..989a5b8d20 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt @@ -17,5 +17,6 @@ data class StartChatState( val userListState: UserListState, val startDmAction: AsyncAction, val isRoomDirectorySearchEnabled: Boolean, + val enableKeyShareOnInvite: Boolean, val eventSink: (StartChatEvents) -> Unit, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt index 448ad1a80a..17d83a9e11 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt @@ -16,6 +16,7 @@ import io.element.android.features.startchat.impl.userlist.aUserListState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.persistentListOf @@ -52,7 +53,7 @@ open class StartChatStateProvider : PreviewParameterProvider { ) ), aCreateRoomRootState( - startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()), + startDmAction = aConfirmingStartDmWithMatrixUser() ), aCreateRoomRootState( isRoomDirectorySearchEnabled = true, @@ -60,6 +61,16 @@ open class StartChatStateProvider : PreviewParameterProvider { ) } +fun aConfirmingStartDmWithMatrixUser( + matrixUser: MatrixUser = aMatrixUser(), + isUserIdentityUnknown: Boolean = false +): ConfirmingStartDmWithMatrixUser { + return ConfirmingStartDmWithMatrixUser( + matrixUser, + isUserIdentityUnknown + ) +} + fun aCreateRoomRootState( applicationName: String = "Element X Preview", userListState: UserListState = aUserListState(), @@ -71,5 +82,6 @@ fun aCreateRoomRootState( userListState = userListState, startDmAction = startDmAction, isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, + enableKeyShareOnInvite = false, eventSink = eventSink, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt index 0b8da1bd94..28bf52549e 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt @@ -130,6 +130,8 @@ fun StartChatView( if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, + isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { state.eventSink(StartChatEvents.StartDM(data.matrixUser)) }, diff --git a/features/startchat/impl/src/main/res/values-ja/translations.xml b/features/startchat/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..ed490becfc --- /dev/null +++ b/features/startchat/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,12 @@ + + + "新しいルーム" + "ルーム階層" + "新しい会話を開始する際に問題が発生しました。" + "アドレスからルームに参加" + "有効なアドレスではありません" + "入力してください…" + "ルームが見つかりました" + "ルームが見つかりません" + "例) #room-name:matrix.org" + diff --git a/features/startchat/impl/src/main/res/values-vi/translations.xml b/features/startchat/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..765b5bc302 --- /dev/null +++ b/features/startchat/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,6 @@ + + + "Phòng mới" + "Danh sách phòng" + "Đã xảy ra lỗi khi cố gắng bắt đầu cuộc trò chuyện" + diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt index 122775f2cc..88b935e47d 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/DefaultStartDMActionTest.kt @@ -13,14 +13,21 @@ import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.CreatedRoom import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.test.runTest import org.junit.Test @@ -67,7 +74,12 @@ class DefaultStartDMActionTest { @Test fun `when dm is not found, and createIfDmDoesNotExist is false, assert dm is not created and state is updated to confirmation state`() = runTest { - val matrixClient = FakeMatrixClient().apply { + val encryptionService = FakeEncryptionService( + getUserIdentityResult = { Result.success(null) } + ) + val matrixClient = FakeMatrixClient( + encryptionService = encryptionService + ).apply { givenFindDmResult(Result.success(null)) givenCreateDmResult(Result.success(A_ROOM_ID)) } @@ -76,7 +88,7 @@ class DefaultStartDMActionTest { val state = mutableStateOf>(AsyncAction.Uninitialized) val matrixUser = aMatrixUser() action.execute(matrixUser, false, state) - assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser)) + assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false)) assertThat(analyticsService.capturedEvents).isEmpty() } @@ -94,13 +106,38 @@ class DefaultStartDMActionTest { assertThat(analyticsService.capturedEvents).isEmpty() } + @Test + fun `when history sharing enabled, user identity fetched and identity unknown`() = runTest { + val getUserIdentityResult = lambdaRecorder> { _ -> Result.success(null) } + val encryptionService = FakeEncryptionService(getUserIdentityResult = getUserIdentityResult) + val matrixClient = FakeMatrixClient(encryptionService = encryptionService).apply { + givenFindDmResult(Result.success(null)) + } + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true) + } + + val action = createStartDMAction( + matrixClient = matrixClient, + featureFlagService = featureFlagService + ) + val state = mutableStateOf>(AsyncAction.Uninitialized) + + action.execute(aMatrixUser(), false, state) + + assertThat(getUserIdentityResult.assertions().isCalledOnce()) + assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = true)) + } + private fun createStartDMAction( matrixClient: MatrixClient = FakeMatrixClient(), analyticsService: AnalyticsService = FakeAnalyticsService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService() ): DefaultStartDMAction { return DefaultStartDMAction( matrixClient = matrixClient, analyticsService = analyticsService, + featureFlagService = featureFlagService, ) } } diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt index 7c209d9052..2bc15e989d 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt @@ -102,7 +102,7 @@ class StartChatPresenterTest { @Test fun `present - start DM action confirmation scenario - cancel`() = runTest { val matrixUser = MatrixUser(UserId("@name:domain")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } @@ -130,7 +130,7 @@ class StartChatPresenterTest { @Test fun `present - start DM action confirmation scenario - confirm`() = runTest { val matrixUser = MatrixUser(UserId("@name:domain")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt index 0e0016ee14..e2a309c17f 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt @@ -26,6 +26,7 @@ data class UserProfileState( val dmRoomId: RoomId?, val canCall: Boolean, val snackbarMessage: SnackbarMessage?, + val enableKeyShareOnInvite: Boolean, val eventSink: (UserProfileEvents) -> Unit ) { enum class ConfirmationDialog { diff --git a/features/userprofile/impl/build.gradle.kts b/features/userprofile/impl/build.gradle.kts index 0b65441cc3..3e68fb2b9c 100644 --- a/features/userprofile/impl/build.gradle.kts +++ b/features/userprofile/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.androidutils) implementation(projects.libraries.mediaviewer.api) + implementation(projects.libraries.featureflag.api) implementation(projects.features.call.api) implementation(projects.features.enterprise.api) implementation(projects.features.verifysession.api) @@ -46,6 +47,7 @@ dependencies { testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaviewer.test) + testImplementation(projects.libraries.featureflag.test) testImplementation(projects.features.call.test) testImplementation(projects.features.verifysession.test) testImplementation(projects.features.startchat.test) diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 7e09a03ec3..a451d86b70 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -31,6 +32,8 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -50,6 +53,7 @@ class UserProfilePresenter( private val client: MatrixClient, private val startDMAction: StartDMAction, private val sessionEnterpriseService: SessionEnterpriseService, + private val featureFlagService: FeatureFlagService, ) : Presenter { @AssistedFactory interface Factory { @@ -101,6 +105,8 @@ class UserProfilePresenter( } val userProfile by produceState(null) { value = client.getProfile(userId).getOrNull() } + val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false) + fun handleEvent(event: UserProfileEvents) { when (event) { is UserProfileEvents.BlockUser -> { @@ -153,6 +159,7 @@ class UserProfilePresenter( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = null, + enableKeyShareOnInvite = enableKeyShareOnInvite.value, eventSink = ::handleEvent, ) } diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index 511effe750..1325b46bc0 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.impl.root.UserProfilePresenter import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -324,7 +325,7 @@ class UserProfilePresenterTest { @Test fun `present - start DM action confirmation scenario - cancel`() = runTest { val matrixUser = MatrixUser(UserId("@alice:server.org")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } @@ -354,7 +355,7 @@ class UserProfilePresenterTest { @Test fun `present - start DM action confirmation scenario - confirm`() = runTest { val matrixUser = MatrixUser(UserId("@alice:server.org")) - val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser) + val startDMConfirmationResult = ConfirmingStartDmWithMatrixUser(matrixUser, false) val executeResult = lambdaRecorder>, Unit> { _, _, actionState -> actionState.value = startDMConfirmationResult } @@ -414,6 +415,7 @@ class UserProfilePresenterTest { sessionEnterpriseService = FakeSessionEnterpriseService( isElementCallAvailableResult = { isElementCallAvailable }, ), + featureFlagService = FakeFeatureFlagService() ) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt index 49a2fee4b5..a4bbcd6aa4 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt @@ -31,7 +31,7 @@ open class UserProfileStateProvider : PreviewParameterProvider aUserProfileState(isBlocked = AsyncData.Loading(true), verificationState = UserProfileVerificationState.UNKNOWN), aUserProfileState(startDmActionState = AsyncAction.Loading), aUserProfileState(canCall = true), - aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser())), + aUserProfileState(startDmActionState = ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = false)), aUserProfileState(verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION), ) } @@ -61,5 +61,6 @@ fun aUserProfileState( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = snackbarMessage, + enableKeyShareOnInvite = false, eventSink = eventSink, ) diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index 380bb006ab..34f992f77d 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -114,6 +114,8 @@ fun UserProfileView( if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, + isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { state.eventSink(UserProfileEvents.StartDM) }, diff --git a/features/userprofile/shared/src/main/res/values-ja/translations.xml b/features/userprofile/shared/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..33ba6b6a85 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-ja/translations.xml @@ -0,0 +1,19 @@ + + + "ブロック" + "ブロックしたユーザーのメッセージは非表示になり、新しく送信することもできません。ブロックはいつでも解除することができます。" + "ユーザーをブロック" + "ブロックを解除" + "すべてのメッセージが再表示されます。" + "ユーザーのブロックを解除" + "ブロック" + "ブロックしたユーザーのメッセージは非表示になり、新しく送信することもできません。ブロックはいつでも解除することができます。" + "ユーザーをブロック" + "プロフィール" + "ブロックを解除" + "すべてのメッセージが再表示されます。" + "ユーザーのブロックを解除" + "このユーザーを検証するにはWeb版アプリを使用してください。" + "%1$s を検証" + "新しい会話を開始する際に問題が発生しました。" + diff --git a/features/userprofile/shared/src/main/res/values-vi/translations.xml b/features/userprofile/shared/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..122d8a2153 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-vi/translations.xml @@ -0,0 +1,16 @@ + + + "Chặn" + "Người dùng bị chặn sẽ không thể gửi tin nhắn cho bạn và tất cả tin nhắn của họ sẽ bị ẩn. Bạn có thể bỏ chặn họ bất cứ lúc nào." + "Chặn người dùng" + "Bỏ chặn" + "Bạn sẽ có thể xem lại tất cả tin nhắn từ họ." + "Bỏ chặn người dùng" + "Chặn" + "Người dùng bị chặn sẽ không thể gửi tin nhắn cho bạn và tất cả tin nhắn của họ sẽ bị ẩn. Bạn có thể bỏ chặn họ bất cứ lúc nào." + "Chặn người dùng" + "Bỏ chặn" + "Bạn sẽ có thể xem lại tất cả tin nhắn từ họ." + "Bỏ chặn người dùng" + "Đã xảy ra lỗi khi cố gắng bắt đầu cuộc trò chuyện" + diff --git a/features/userprofile/shared/src/main/res/values-zh/translations.xml b/features/userprofile/shared/src/main/res/values-zh/translations.xml index b1b37bdbdc..38c4e3e8eb 100644 --- a/features/userprofile/shared/src/main/res/values-zh/translations.xml +++ b/features/userprofile/shared/src/main/res/values-zh/translations.xml @@ -1,18 +1,18 @@ - "封禁" - "被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。" - "封禁用户" - "解封" + "屏蔽" + "被屏蔽的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解除屏蔽。" + "屏蔽用户" + "解除屏蔽" "可以重新接收他们的消息。" - "解封用户" - "封禁" - "被封禁的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解封。" - "封禁用户" + "解除屏蔽用户" + "屏蔽" + "被屏蔽的用户无法给你发消息,并且他们的消息会被隐藏。你可以随时解除屏蔽。" + "屏蔽用户" "个人资料" - "解封" + "解除屏蔽" "可以重新接收他们的消息。" - "解封用户" + "解除屏蔽用户" "使用 Web 应用程序验证此用户。" "验证 %1$s" "在开始聊天时发生了错误" diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml index 5d0b28bc0a..3c3e68e1f0 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -2,8 +2,8 @@ "Nemůžete potvrdit?" "Vytvoření nového klíče pro obnovení" - "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." - "Potvrďte, že jste to vy" + "Vyberte způsob ověření pro nastavení zabezpečeného zasílání zpráv." + "Potvrďte svou digitální identitu" "Použít jiné zařízení" "Použít klíč pro obnovení" "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." @@ -17,7 +17,7 @@ "Potvrďte, že níže uvedená čísla odpovídají číslům zobrazeným na vaší druhé relaci." "Porovnejte čísla" "Nyní můžete bezpečně číst nebo odesílat zprávy na svém druhém zařízení." - "Nyní můžete důvěřovat identitě tohoto uživatele při odesílání nebo přijímání zpráv." + "Nyní můžete při odesílání nebo přijímání zpráv důvěřovat digitální identitě tohoto uživatele." "Zařízení ověřeno" "Zadejte klíč pro obnovení" "Buď vypršel časový limit požadavku, požadavek byl zamítnut, nebo došlo k nesouladu ověření." @@ -42,7 +42,7 @@ "Otevřete aplikaci na jiném ověřeném zařízení" "Pro větší bezpečnost ověřte tohoto uživatele porovnáním sady emotikonů na svých zařízeních. Proveďte to pomocí důvěryhodného způsobu komunikace." "Ověřte tohoto uživatele?" - "Pro větší bezpečnost chce jiný uživatel ověřit vaši identitu. Zobrazí se vám sada emotikonů k porovnání." + "Z důvodu zvýšené bezpečnosti chce jiný uživatel ověřit vaši digitální identitu. Zobrazí se vám sada emodži, které je třeba porovnat." "Na druhém zařízení byste měli vidět vyskakovací okno. Začněte s ověrením tam." "Spusťte ověření na druhém zařízení" "Spusťte ověření na druhém zařízení" @@ -50,5 +50,5 @@ "Po přijetí budete moci pokračovat v ověřování." "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci." "Čekání na přijetí žádosti" - "Odhlašování…" + "Odebírání zařízení…" diff --git a/features/verifysession/impl/src/main/res/values-da/translations.xml b/features/verifysession/impl/src/main/res/values-da/translations.xml index 32cdf159d5..90e227eae6 100644 --- a/features/verifysession/impl/src/main/res/values-da/translations.xml +++ b/features/verifysession/impl/src/main/res/values-da/translations.xml @@ -2,8 +2,8 @@ "Kan ikke bekræfte?" "Opret en ny gendannelsesnøgle" - "Verificér denne enhed for at konfigurere sikre meddelelser." - "Bekræft din identitet" + "Vælg, hvordan du vil verificere dig for at konfigurere sikre beskeder." + "Bekræft din digitale identitet" "Brug en anden enhed" "Brug gendannelsesnøgle" "Nu kan du læse eller sende beskeder sikkert, og enhver du samtaler med kan også stole på denne enhed." @@ -17,7 +17,7 @@ "Bekræft, at numrene nedenfor stemmer overens med dem, der vises på din anden session." "Sammenlign tal" "Nu kan du læse eller sende beskeder sikkert med din anden enhed." - "Nu kan du stole på identiteten af denne bruger, når I sender og modtager beskeder fra hinanden." + "Nu kan du stole på denne brugers digitale identitet, når I sender eller modtager beskeder." "Enhed verificeret" "Indtast gendannelsesnøgle" "Enten udløb anmodningen, den blev afvist, eller der var en fejl i verifikationen." @@ -42,7 +42,7 @@ "Åbn appen på en anden bekræftet enhed" "For ekstra sikkerhed, verificér denne bruger ved at sammenligne et sæt emojier på jeres enheder. Gør dette ved at bruge en kommunikationsmetode i stoler på." "Verificér denne bruger?" - "For ekstra sikkerhed ønsker en anden bruger at bekræfte din identitet. Du får vist et sæt emojier til sammenligning." + "For ekstra sikkerhed ønsker en anden bruger at bekræfte din digitale identitet. I vil blive vist et sæt emojis, der skal sammenlignes." "Du burde se en popup på den anden enhed. Start verifikationen derfra nu." "Start verifikation på den anden enhed" "Start verifikation på den anden enhed" @@ -50,5 +50,5 @@ "Når du er blevet accepteret, kan du fortsætte med verifikationen." "Accepter anmodningen om at starte bekræftelsesprocessen i din anden session for at fortsætte." "Venter på at acceptere anmodningen" - "Logger ud…" + "Fjerner enhed…" diff --git a/features/verifysession/impl/src/main/res/values-hu/translations.xml b/features/verifysession/impl/src/main/res/values-hu/translations.xml index b7617168ee..666ff26af1 100644 --- a/features/verifysession/impl/src/main/res/values-hu/translations.xml +++ b/features/verifysession/impl/src/main/res/values-hu/translations.xml @@ -50,5 +50,5 @@ "Az elfogadása után folytathatja az ellenőrzést." "A folytatáshoz fogadja el az ellenőrzési folyamat indítási kérését a másik munkamenetében." "Várakozás a kérés elfogadására" - "Kijelentkezés…" + "Eszköz eltávolítása…" diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 5b11c27cbe..2bb22e98f9 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -2,8 +2,8 @@ "Non puoi confermare?" "Crea una nuova chiave di recupero" - "Verifica questo dispositivo per segnare i tuoi messaggi come sicuri." - "Conferma la tua identità" + "Scegli come effettuare la verifica per configurare la messaggistica sicura." + "Conferma la tua identità digitale" "Usa un altro dispositivo" "Usa la chiave di recupero" "Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo." @@ -17,7 +17,7 @@ "Conferma che i numeri seguenti corrispondano a quelli mostrati nell\'altra sessione." "Confronta i numeri" "Ora puoi leggere o inviare messaggi in modo sicuro sul tuo altro dispositivo." - "Ora puoi fidarti dell\'identità di questo utente quando invii o ricevi messaggi." + "Ora puoi fidarti dell\'identità digitale di questo utente quando invii o ricevi messaggi." "Dispositivo verificato" "Inserisci la chiave di recupero" "La richiesta è scaduta, è stata rifiutata o c\'è stata una mancata corrispondenza nella verifica." @@ -42,7 +42,7 @@ "Apri l\'app su un altro dispositivo verificato" "Per una maggiore sicurezza, verifica questo utente confrontando un set di emoji sui tuoi dispositivi. A tale scopo, utilizza un metodo di comunicazione affidabile." "Verificare questo utente?" - "Per una maggiore sicurezza, un altro utente desidera verificare la tua identità. Ti verrà mostrato un set di emoji da confrontare." + "Per maggiore sicurezza, un altro utente vuole verificare la tua identità digitale. Ti verrà mostrata una serie di emoji da confrontare." "Dovresti vedere un popup sull\'altro dispositivo. Inizia subito la verifica da lì." "Avvia la verifica sull\'altro dispositivo" "Avvia la verifica sull\'altro dispositivo" @@ -50,5 +50,5 @@ "Una volta accettata, potrai proseguire con la verifica." "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." "In attesa di accettare la richiesta" - "Disconnessione in corso…" + "Rimozione del dispositivo…" diff --git a/features/verifysession/impl/src/main/res/values-ja/translations.xml b/features/verifysession/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..72360fda21 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,54 @@ + + + "認証できませんか?" + "回復鍵を新規作成します" + "安全なメッセージを設定するための検証方法を選択してください。" + "デジタルIDの認証" + "他の端末を使用" + "回復鍵を使用" + "メッセージのやり取りを安全に行えるようになりました。他のユーザーはこの端末を信頼できます。" + "検証済みの端末" + "他の端末を使用" + "一方の端末を待機中…" + "問題が発生しました。リクエストがタイムアウトまたは拒否されました。" + "以下の絵文字が、もう一方の端末の表示と一致することを確認してください。" + "絵文字の比較" + "一方のユーザーの端末上に表示される絵文字と一致することを確認してください。" + "もう一方のセッションと数字が一致することを確認してください。" + "数字を比較してください" + "もう一方の端末でも、安全なメッセージのやり取りが可能になりました。" + "メッセージのやり取りにおいて、このユーザーのデジタルIDを信頼できるようになりました。" + "検証済みの端末" + "回復鍵を入力" + "リクエストがタイムアウトしたか、リクエストの拒否あるいは検証に不一致がありました。" + "暗号化された過去のメッセージを確認するには本人検証が必要です。" + "既存のセッションを使用" + "検証を再試行" + "検証を実行" + "一致を待機中…" + "絵文字の組み合わせを比較してください。" + "絵文字が双方で一致して表示されていることを確認してください。" + "サインイン済み" + "リクエストがタイムアウトしたか、リクエストの拒否あるいは検証に不一致がありました。" + "検証に失敗しました" + "あなたが検証を開始した場合にのみ続行してください。" + "他の端末を検証して過去のメッセージの安全を保ってください。" + "もう一方の端末でも、安全なメッセージのやり取りが可能になりました。" + "検証済みの端末" + "検証をリクエスト済み" + "一致しません" + "一致します" + "検証を開始する前に、他の端末でアプリケーションを開いてください。" + "検証済みの他の端末でアプリケーションを開いてください" + "安全性を高めるために、絵文字の組み合わせを使用してこのユーザーを検証してください。これにより安全にやり取りを行うことができるようになります。" + "このユーザーを検証しますか?" + "安全性を高めるために、相手ユーザーがあなたのデジタルIDを検証することを要求しています。比較用の絵文字の組み合わせが表示されます。" + "一方の端末でポップアップが表示されます。そこから検証を開始してください。" + "一方の端末で検証を開始してください" + "一方の端末で検証を開始してください" + "一方の端末を待機中" + "検証を承認することで続行できます。" + "他の端末で検証リクエストを承認してください。" + "リクエストの承認を待機中" + "削除中…" + diff --git a/features/verifysession/impl/src/main/res/values-ko/translations.xml b/features/verifysession/impl/src/main/res/values-ko/translations.xml index 262a4d3052..493ac0d685 100644 --- a/features/verifysession/impl/src/main/res/values-ko/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ko/translations.xml @@ -17,7 +17,7 @@ "아래 숫자가 다른 세션에 표시된 숫자와 일치하는지 확인하세요." "숫자 비교" "이제 다른 기기에서도 안전하게 메시지를 읽거나 보낼 수 있습니다." - "이제 메시지를 보내거나 받을 때 이 사용자의 신원을 신뢰할 수 있습니다." + "이제 메시지를 주고받을 때 이 사용자의 디지털 신원을 신뢰할 수 있습니다." "기기 검증됨" "복구 키를 입력하세요" "요청이 시간 초과되었거나, 요청이 거부되었거나, 검증 불일치가 발생했습니다." @@ -42,7 +42,7 @@ "다른 검증된 장치에서 앱을 실행하세요" "보안을 강화하려면, 기기에 표시된 이모티콘을 비교하여 이 사용자를 확인하세요. 신뢰할 수 있는 통신 수단을 사용하여 확인하시기 바랍니다." "이 사용자를 검증하시겠습니까?" - "추가 보안 위해 다른 사용자가 귀하의 신원을 확인하고자 합니다. 비교할 이모티콘 세트가 표시됩니다." + "보안 강화를 위해 상대방이 귀하의 디지털 신원을 확인하려고 합니다. 화면에 표시되는 이모지 세트가 서로 일치하는지 비교해 주세요." "다른 기기에 팝업이 표시될 것입니다. 지금 그곳에서 확인을 시작하세요." "다른 장치에서 검증 시작" "다른 장치에서 검증 시작" diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml index 8672229640..91974caa1f 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -2,7 +2,7 @@ "Не можете подтвердить?" "Создайте новый ключ восстановления" - "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." + "Выберите способ подтверждения для настройки защищенного обмена сообщениями." "Подтвердите личность" "Использовать другое устройство" "Использовать ключ восстановления" @@ -17,7 +17,7 @@ "Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе." "Сравните числа" "Теперь вы можете безопасно читать или отправлять сообщения на новом устройстве." - "Теперь вы можете доверять сообщениям этого пользователя." + "Теперь ты можешь доверять цифровой идентичности этого пользователя при отправке или получении сообщений." "Устройство проверено" "Введите ключ восстановления" "Время ожидания подтверждения истекло, запрос был отклонён, или произошла ошибка." @@ -50,5 +50,5 @@ "После принятия запроса вы сможете продолжить проверку." "Чтобы продолжить, примите запрос на запуск процесса подтверждения в другом сеансе." "Ожидание принятия запроса" - "Выполняется выход…" + "Удаление устройства…" diff --git a/features/verifysession/impl/src/main/res/values-sv/translations.xml b/features/verifysession/impl/src/main/res/values-sv/translations.xml index 100f4964da..9f4008d0a4 100644 --- a/features/verifysession/impl/src/main/res/values-sv/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sv/translations.xml @@ -50,5 +50,5 @@ "När det har accepterats kommer du kunna fortsätta verifieringen." "Godkänn begäran om att starta verifieringsprocessen på din andra session för att fortsätta." "Väntar på att acceptera begäran" - "Loggar ut …" + "Tar bort enhet …" diff --git a/features/verifysession/impl/src/main/res/values-vi/translations.xml b/features/verifysession/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..7cb5b916cf --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,31 @@ + + + "Chọn phương thức xác minh để bật nhắn tin bảo mật." + "Xác nhận danh tính kỹ thuật số của bạn" + "Giờ đây bạn có thể đọc và gửi tin nhắn một cách an toàn, và những người bạn trò chuyện cũng có thể tin tưởng thiết bị này." + "Thiết bị được xác thực" + "Đang chờ trên thiết bị khác…" + "Có vẻ như có điều gì đó không đúng. Hoặc yêu cầu đã hết thời gian chờ hoặc yêu cầu đã bị từ chối." + "Hãy xác nhận rằng các biểu tượng cảm xúc bên dưới khớp với các biểu tượng hiển thị trên thiết bị khác của bạn." + "So sánh các biểu tượng cảm xúc" + "Xác nhận rằng các số bên dưới khớp với số hiển thị trên thiết bị đăng nhập khác của bạn." + "So sánh số liệu" + "Giờ đây, bạn có thể đọc hoặc gửi tin nhắn một cách an toàn trên thiết bị khác của mình." + "Thiết bị được xác thực" + "Nhập mã khôi phục." + "Hãy chứng minh đó là bạn để truy cập vào lịch sử tin nhắn đã mã hóa của bạn." + "Mở một phiên hiện có" + "Thử xác minh lại" + "Tôi đã sẵn sàng" + "Đang chờ ghép đôi…" + "So sánh một bộ biểu tượng cảm xúc duy nhất." + "So sánh các biểu tượng cảm xúc riêng biệt, đảm bảo chúng xuất hiện theo cùng một thứ tự." + "Xác minh thất bại" + "Giờ đây, bạn có thể đọc hoặc gửi tin nhắn một cách an toàn trên thiết bị khác của mình." + "Thiết bị được xác thực" + "Chúng không khớp nhau" + "Chúng khớp với nhau" + "Hãy chấp nhận yêu cầu bắt đầu quá trình xác minh trong phiên làm việc khác của bạn để tiếp tục." + "Đang chờ chấp nhận yêu cầu" + "Đang gỡ thiết bị…" + diff --git a/features/verifysession/impl/src/main/res/values-zh/translations.xml b/features/verifysession/impl/src/main/res/values-zh/translations.xml index 54b6de5d68..b48bfe3f87 100644 --- a/features/verifysession/impl/src/main/res/values-zh/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml @@ -2,8 +2,8 @@ "无法确认?" "创建新的恢复密钥" - "验证此设备以开始安全地收发消息。" - "确认这是你" + "选择验证方式以设置安全的消息传输。" + "确认您的数字身份" "使用其他设备" "使用恢复密钥" "现在,您可以安全地阅读或发送消息,与您聊天的人也会信任此设备。" @@ -17,7 +17,7 @@ "确认以下数字与其他会话中显示的一致。" "比较数字" "现在您可以在其他设备上安全地阅读或发送消息。" - "现在您可以在发送或接收消息时信任该用户的身份。" + "现在您可以在发送或接收消息时信任该用户的数字身份。" "设备已验证" "输入恢复密钥" "要么请求超时,要么请求被拒绝,要么验证不匹配。" @@ -42,7 +42,7 @@ "在另一台验证的设备上打开应用" "为了提高安全性,请通过比较设备上的一组表情符号来验证此用户。通过使用安全方式来做到这一点,如面对面。" "验证此用户?" - "为了提高安全性,另一位用户想要验证您的身份。您将看到一组表情符号供您比较。" + "为了额外的安全性,另一位用户想要验证您的数字身份。您将看到一组表情符号供您比较。" "您应该会在另一台设备上看到一个弹出窗口。现在从那里开始验证。" "在另一台设备上开始验证" "在另一台设备上开始验证" @@ -50,5 +50,5 @@ "一旦被接受,您将能够继续进行验证。" "请在其他会话中接受验证请求。" "等待接受请求" - "正在登出…" + "正在删除设备……" diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt index dce5ffeae3..2a6b4031ef 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Folder import androidx.compose.material.icons.outlined.SubdirectoryArrowLeft import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -111,7 +112,7 @@ private fun ItemRow( } is Item.Folder -> { ListItem( - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Folder())), + leadingContent = ListItemContent.Icon(IconSource.Vector(Icons.Outlined.Folder)), headlineContent = { Text( text = item.name, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2f6d5afea..63428fd6f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,23 +6,23 @@ android_gradle_plugin = "8.13.2" # When updating this, please also update the version in the file ./idea/kotlinc.xml kotlin = "2.3.20" -kotlinpoet = "2.2.0" +kotlinpoet = "2.3.0" ksp = "2.3.6" firebaseAppDistribution = "5.2.1" # AndroidX -core = "1.17.0" +core = "1.18.0" datastore = "1.2.1" constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" lifecycle = "2.10.0" activity = "1.13.0" -media3 = "1.9.3" +media3 = "1.10.0" camera = "1.5.3" -work = "2.11.1" +work = "2.11.2" # Compose -compose_bom = "2026.03.00" +compose_bom = "2026.03.01" # Coroutines coroutines = "1.10.2" @@ -46,15 +46,15 @@ showkase = "1.0.5" # When upgrading this version, check state restoration still works fine. appyx = "1.7.1" sqldelight = "2.3.2" -wysiwyg = "2.41.1" -telephoto = "0.18.0" +wysiwyg = "2.41.3" +telephoto = "0.19.0" haze = "1.7.2" # Dependency analysis dependencyAnalysis = "3.6.1" # DI -metro = "0.11.4" +metro = "0.13.2" # Auto service autoservice = "1.1.1" @@ -64,7 +64,7 @@ detekt = "1.23.8" # See https://github.com/pinterest/ktlint/releases/ ktlint = "1.8.0" androidx-test-ext-junit = "1.3.0" -kover = "0.9.7" +kover = "0.9.8" [libraries] # Project @@ -84,12 +84,12 @@ google_firebase_bom = "com.google.firebase:firebase-bom:34.11.0" firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } -google_tink = "com.google.crypto.tink:tink-android:1.20.0" +google_tink = "com.google.crypto.tink:tink-android:1.21.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } -androidx_annotationjvm = "androidx.annotation:annotation-jvm:1.9.1" +androidx_annotationjvm = "androidx.annotation:annotation-jvm:1.10.0" androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx_exifinterface = "androidx.exifinterface:exifinterface:1.4.2" @@ -102,7 +102,7 @@ androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.0.0" androidx_workmanager_runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } androidx_recyclerview = "androidx.recyclerview:recyclerview:1.4.0" -androidx_browser = "androidx.browser:browser:1.9.0" +androidx_browser = "androidx.browser:browser:1.10.0" androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_splash = "androidx.core:core-splashscreen:1.2.0" @@ -165,10 +165,10 @@ test_mockk = "io.mockk:mockk:1.14.9" test_konsist = "com.lemonappdev:konsist:0.17.3" test_turbine = "app.cash.turbine:turbine:1.2.1" test_truth = "com.google.truth:truth:1.4.5" -test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.21" +test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.22" test_robolectric = "org.robolectric:robolectric:4.16.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } -test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.8.1" +test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.8.2" test_detekt_api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } @@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.03.24" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.15" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -200,14 +200,14 @@ matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } -sqlcipher = "net.zetetic:sqlcipher-android:4.13.0" +sqlcipher = "net.zetetic:sqlcipher-android:4.14.1" sqlite = "androidx.sqlite:sqlite-ktx:2.6.2" unifiedpush = "org.unifiedpush.android:connector:3.3.2" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" -telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } +telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" } telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.2.2" -maplibre = "org.maplibre.gl:android-sdk:13.0.1" +maplibre = "org.maplibre.gl:android-sdk:13.0.2" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_compose = "org.maplibre.compose:maplibre-compose:0.12.1" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" @@ -220,20 +220,20 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics -posthog = "com.posthog:posthog-android:3.37.0" -sentry = "io.sentry:sentry-android:8.36.0" +posthog = "com.posthog:posthog-android:3.39.0" +sentry = "io.sentry:sentry-android:8.37.1" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" # Emojibase -matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.5.1" +matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.5.3" sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0" # Di metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } # Element Call -element_call_embedded = "io.element.android:element-call-embedded:0.18.0" +element_call_embedded = "io.element.android:element-call-embedded:0.19.1" # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/service/ServiceBinder.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/service/ServiceBinder.kt new file mode 100644 index 0000000000..ba71ca131f --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/service/ServiceBinder.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.service + +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.di.annotations.ApplicationContext + +interface ServiceBinder { + fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean + fun unbindService(conn: ServiceConnection) +} + +@ContributesBinding(AppScope::class) +class DefaultServiceBinder( + @ApplicationContext private val context: Context, +) : ServiceBinder { + override fun bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean { + return context.bindService(service, conn, flags) + } + + override fun unbindService(conn: ServiceConnection) { + context.unbindService(conn) + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt index ae724a7c44..5f19cba136 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/ui/View.kt @@ -16,9 +16,11 @@ import android.view.ViewTreeObserver import android.view.WindowInsets import android.view.inputmethod.InputMethodManager import androidx.core.content.getSystemService +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withTimeoutOrNull import kotlin.coroutines.resume +import kotlin.time.Duration.Companion.seconds fun View.hideKeyboard() { val imm = context?.getSystemService() @@ -26,29 +28,39 @@ fun View.hideKeyboard() { } suspend fun View.hideKeyboardAndAwaitAnimation() { - val imm = context?.getSystemService() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !rootWindowInsets.isVisible(WindowInsets.Type.ime())) { + // Keyboard is already hidden, no need to do anything + return + } - val mutex = Mutex() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val imm = context?.getSystemService() ?: return + val future = CompletableDeferred() + + val requested = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { setOnApplyWindowInsetsListener { view, insets -> if (!insets.isVisible(WindowInsets.Type.ime())) { - mutex.unlock() + future.complete(Unit) + // Remove the listener now, it's a single use operation + setOnApplyWindowInsetsListener(null) } insets } - imm?.hideSoftInputFromWindow(windowToken, 0) + imm.hideSoftInputFromWindow(windowToken, 0) } else { @Suppress("DEPRECATION") - imm?.hideSoftInputFromWindow(windowToken, 0, object : ResultReceiver(null) { + imm.hideSoftInputFromWindow(windowToken, 0, object : ResultReceiver(null) { override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { - if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN || - resultCode == InputMethodManager.RESULT_HIDDEN) { - mutex.unlock() + if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN || resultCode == InputMethodManager.RESULT_HIDDEN) { + future.complete(Unit) } } }) } - mutex.lock() + + if (requested) { + // Await the future to ensure the keyboard hide animation has completed before proceeding + withTimeoutOrNull(1.seconds) { future.await() } + } } fun View.showKeyboard(andRequestFocus: Boolean = false) { diff --git a/libraries/androidutils/src/main/res/values-ja/translations.xml b/libraries/androidutils/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c27f0f3ed2 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ja/translations.xml @@ -0,0 +1,4 @@ + + + "このアクションを処理できるアプリが見つかりません。" + diff --git a/libraries/androidutils/src/main/res/values-vi/translations.xml b/libraries/androidutils/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..bf30372f4a --- /dev/null +++ b/libraries/androidutils/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "Không tìm thấy ứng dụng tương thích nào để xử lý hành động này." + diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/FaderOrSliderTransitionHandler.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/FaderOrSliderTransitionHandler.kt new file mode 100644 index 0000000000..c8da7439a5 --- /dev/null +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/FaderOrSliderTransitionHandler.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.architecture.appyx + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler +import com.bumble.appyx.core.navigation.transition.TransitionDescriptor +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.NewRoot +import com.bumble.appyx.navmodel.backstack.operation.Replace +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider + +/** + * A TransitionHandler that uses fade transition when the operation is Replace or NewRoot, + * and slide transition for all other cases. + */ +private class FaderOrSliderTransitionHandler( + private val slider: ModifierTransitionHandler, + private val fader: ModifierTransitionHandler, +) : ModifierTransitionHandler() { + override fun createModifier( + modifier: Modifier, + transition: Transition, + descriptor: TransitionDescriptor + ): Modifier { + val operation = descriptor.operation + val useFader = operation is Replace || operation is NewRoot + val handler = if (useFader) fader else slider + return handler.createModifier(modifier, transition, descriptor) + } +} + +@Composable +fun rememberFaderOrSliderTransitionHandler(): ModifierTransitionHandler { + val slider = rememberBackstackSlider( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + val fader = rememberBackstackFader( + transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) }, + ) + return rememberDelegateTransitionHandler { + FaderOrSliderTransitionHandler(slider, fader) + } +} diff --git a/libraries/compound/screenshots/Compound Icons - Dark.png b/libraries/compound/screenshots/Compound Icons - Dark.png index f140517dff..2e3fd0d04d 100644 --- a/libraries/compound/screenshots/Compound Icons - Dark.png +++ b/libraries/compound/screenshots/Compound Icons - Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37f6acca46890e98087ece62e2716fa60791479fab02999406050517e3b79307 -size 240187 +oid sha256:7901fea2f578c8ed796160c9c08f417c61f4fb21580f958844fdf0cb794adf8a +size 239731 diff --git a/libraries/compound/screenshots/Compound Icons - Light.png b/libraries/compound/screenshots/Compound Icons - Light.png index c84421b6fa..6e1bfc80cd 100644 --- a/libraries/compound/screenshots/Compound Icons - Light.png +++ b/libraries/compound/screenshots/Compound Icons - Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2de5e6d24dcbe0baa75a69485f5a308466fa599625bcbdb0cb96e9bc5a1b708 -size 253233 +oid sha256:245f012d419817f6557d92a71729b3b70092f24f0eba37f2f1fc431ad27592be +size 252969 diff --git a/libraries/compound/screenshots/Compound Icons - Rtl.png b/libraries/compound/screenshots/Compound Icons - Rtl.png index 89be63840a..aa1b5d93a7 100644 --- a/libraries/compound/screenshots/Compound Icons - Rtl.png +++ b/libraries/compound/screenshots/Compound Icons - Rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae1cb46d82acbb23cc172f41e20a41bbe88c350ab53c20e5b2a91f2c16590fbf -size 254525 +oid sha256:2d15c52b21cc279d306fa187cd0c318820109b5ec66270e6447e1b02e800eeba +size 254206 diff --git a/libraries/compound/screenshots/Compound Vector Icons - Dark.png b/libraries/compound/screenshots/Compound Vector Icons - Dark.png index 702fd9b425..a01fbf18b4 100644 --- a/libraries/compound/screenshots/Compound Vector Icons - Dark.png +++ b/libraries/compound/screenshots/Compound Vector Icons - Dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a8a9b6e61758a40d01028a4edb4a4d21b845b83b3e0793ed0934e48f3d9eea0 -size 94637 +oid sha256:85ef188fa3a27e42f4beafc899c1f3e7e8bcfad980ed76af6a03f76d70d6a511 +size 93807 diff --git a/libraries/compound/screenshots/Compound Vector Icons - Light.png b/libraries/compound/screenshots/Compound Vector Icons - Light.png index 76b1cc49bd..e227f1579c 100644 --- a/libraries/compound/screenshots/Compound Vector Icons - Light.png +++ b/libraries/compound/screenshots/Compound Vector Icons - Light.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f29d225df71587fefe07ec8739b84f1a0469786c6b1d6778da0bad33d19574e -size 101183 +oid sha256:f6e38386e95dc0c50384f06fca122ce14851ceff8ffc7865e394c1b4fccc5db6 +size 100555 diff --git a/libraries/compound/src/main/assets/theme.iife.js b/libraries/compound/src/main/assets/theme.iife.js index 1dd693cbd4..e7205e4f81 100644 --- a/libraries/compound/src/main/assets/theme.iife.js +++ b/libraries/compound/src/main/assets/theme.iife.js @@ -1 +1 @@ -var CompoundTheme=(function($t){"use strict";const{min:oi,max:si}=Math,qt=(e,t=0,r=1)=>oi(si(t,e),r),xe=e=>{e._clipped=!1,e._unclipped=e.slice(0);for(let t=0;t<=3;t++)t<3?((e[t]<0||e[t]>255)&&(e._clipped=!0),e[t]=qt(e[t],0,255)):t===3&&(e[t]=qt(e[t],0,1));return e},fn={};for(let e of["Boolean","Number","String","Function","Array","Date","RegExp","Undefined","Null"])fn[`[object ${e}]`]=e.toLowerCase();function N(e){return fn[Object.prototype.toString.call(e)]||"object"}const E=(e,t=null)=>e.length>=3?Array.prototype.slice.call(e):N(e[0])=="object"&&t?t.split("").filter(r=>e[0][r]!==void 0).map(r=>e[0][r]):e[0].slice(0),Et=e=>{if(e.length<2)return null;const t=e.length-1;return N(e[t])=="string"?e[t].toLowerCase():null},{PI:Wt,min:hn,max:dn}=Math,et=e=>Math.round(e*100)/100,Ce=e=>Math.round(e*100)/100,ft=Wt*2,ke=Wt/3,ii=Wt/180,ai=180/Wt;function bn(e){return[...e.slice(0,3).reverse(),...e.slice(3)]}const $={format:{},autodetect:[]};let _=class{constructor(...t){const r=this;if(N(t[0])==="object"&&t[0].constructor&&t[0].constructor===this.constructor)return t[0];let n=Et(t),o=!1;if(!n){o=!0,$.sorted||($.autodetect=$.autodetect.sort((i,s)=>s.p-i.p),$.sorted=!0);for(let i of $.autodetect)if(n=i.test(...t),n)break}if($.format[n]){const i=$.format[n].apply(null,o?t:t.slice(0,-1));r._rgb=xe(i)}else throw new Error("unknown format: "+t);r._rgb.length===3&&r._rgb.push(1)}toString(){return N(this.hex)=="function"?this.hex():`[${this._rgb.join(",")}]`}};const ci="3.2.0",k=(...e)=>new _(...e);k.version=ci;const Lt={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",laserlemon:"#ffff54",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrod:"#fafad2",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",maroon2:"#7f0000",maroon3:"#b03060",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",purple2:"#7f007f",purple3:"#a020f0",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"},ui=/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,li=/^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$/,pn=e=>{if(e.match(ui)){(e.length===4||e.length===7)&&(e=e.substr(1)),e.length===3&&(e=e.split(""),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]);const t=parseInt(e,16),r=t>>16,n=t>>8&255,o=t&255;return[r,n,o,1]}if(e.match(li)){(e.length===5||e.length===9)&&(e=e.substr(1)),e.length===4&&(e=e.split(""),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]+e[3]+e[3]);const t=parseInt(e,16),r=t>>24&255,n=t>>16&255,o=t>>8&255,i=Math.round((t&255)/255*100)/100;return[r,n,o,i]}throw new Error(`unknown hex color: ${e}`)},{round:Ut}=Math,mn=(...e)=>{let[t,r,n,o]=E(e,"rgba"),i=Et(e)||"auto";o===void 0&&(o=1),i==="auto"&&(i=o<1?"rgba":"rgb"),t=Ut(t),r=Ut(r),n=Ut(n);let a="000000"+(t<<16|r<<8|n).toString(16);a=a.substr(a.length-6);let c="0"+Ut(o*255).toString(16);switch(c=c.substr(c.length-2),i.toLowerCase()){case"rgba":return`#${a}${c}`;case"argb":return`#${c}${a}`;default:return`#${a}`}};_.prototype.name=function(){const e=mn(this._rgb,"rgb");for(let t of Object.keys(Lt))if(Lt[t]===e)return t.toLowerCase();return e},$.format.named=e=>{if(e=e.toLowerCase(),Lt[e])return pn(Lt[e]);throw new Error("unknown color name: "+e)},$.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&N(e)==="string"&&Lt[e.toLowerCase()])return"named"}}),_.prototype.alpha=function(e,t=!1){return e!==void 0&&N(e)==="number"?t?(this._rgb[3]=e,this):new _([this._rgb[0],this._rgb[1],this._rgb[2],e],"rgb"):this._rgb[3]},_.prototype.clipped=function(){return this._rgb._clipped||!1};const ct={Kn:18,labWhitePoint:"d65",Xn:.95047,Yn:1,Zn:1.08883,kE:216/24389,kKE:8,kK:24389/27,RefWhiteRGB:{X:.95047,Y:1,Z:1.08883},MtxRGB2XYZ:{m00:.4124564390896922,m01:.21267285140562253,m02:.0193338955823293,m10:.357576077643909,m11:.715152155287818,m12:.11919202588130297,m20:.18043748326639894,m21:.07217499330655958,m22:.9503040785363679},MtxXYZ2RGB:{m00:3.2404541621141045,m01:-.9692660305051868,m02:.055643430959114726,m10:-1.5371385127977166,m11:1.8760108454466942,m12:-.2040259135167538,m20:-.498531409556016,m21:.041556017530349834,m22:1.0572251882231791},As:.9414285350000001,Bs:1.040417467,Cs:1.089532651,MtxAdaptMa:{m00:.8951,m01:-.7502,m02:.0389,m10:.2664,m11:1.7135,m12:-.0685,m20:-.1614,m21:.0367,m22:1.0296},MtxAdaptMaI:{m00:.9869929054667123,m01:.43230526972339456,m02:-.008528664575177328,m10:-.14705425642099013,m11:.5183602715367776,m12:.04004282165408487,m20:.15996265166373125,m21:.0492912282128556,m22:.9684866957875502}},fi=new Map([["a",[1.0985,.35585]],["b",[1.0985,.35585]],["c",[.98074,1.18232]],["d50",[.96422,.82521]],["d55",[.95682,.92149]],["d65",[.95047,1.08883]],["e",[1,1,1]],["f2",[.99186,.67393]],["f7",[.95041,1.08747]],["f11",[1.00962,.6435]],["icc",[.96422,.82521]]]);function ht(e){const t=fi.get(String(e).toLowerCase());if(!t)throw new Error("unknown Lab illuminant "+e);ct.labWhitePoint=e,ct.Xn=t[0],ct.Zn=t[1]}function Kt(){return ct.labWhitePoint}const Re=(...e)=>{e=E(e,"lab");const[t,r,n]=e,[o,i,s]=hi(t,r,n),[a,c,u]=gn(o,i,s);return[a,c,u,e.length>3?e[3]:1]},hi=(e,t,r)=>{const{kE:n,kK:o,kKE:i,Xn:s,Yn:a,Zn:c}=ct,u=(e+16)/116,f=.002*t+u,l=u-.005*r,h=f*f*f,d=l*l*l,b=h>n?h:(116*f-16)/o,g=e>i?Math.pow((e+16)/116,3):e/o,m=d>n?d:(116*l-16)/o,y=b*s,L=g*a,w=m*c;return[y,L,w]},qe=e=>{const t=Math.sign(e);return e=Math.abs(e),(e<=.0031308?e*12.92:1.055*Math.pow(e,1/2.4)-.055)*t},gn=(e,t,r)=>{const{MtxAdaptMa:n,MtxAdaptMaI:o,MtxXYZ2RGB:i,RefWhiteRGB:s,Xn:a,Yn:c,Zn:u}=ct,f=a*n.m00+c*n.m10+u*n.m20,l=a*n.m01+c*n.m11+u*n.m21,h=a*n.m02+c*n.m12+u*n.m22,d=s.X*n.m00+s.Y*n.m10+s.Z*n.m20,b=s.X*n.m01+s.Y*n.m11+s.Z*n.m21,g=s.X*n.m02+s.Y*n.m12+s.Z*n.m22,m=(e*n.m00+t*n.m10+r*n.m20)*(d/f),y=(e*n.m01+t*n.m11+r*n.m21)*(b/l),L=(e*n.m02+t*n.m12+r*n.m22)*(g/h),w=m*o.m00+y*o.m10+L*o.m20,x=m*o.m01+y*o.m11+L*o.m21,S=m*o.m02+y*o.m12+L*o.m22,R=qe(w*i.m00+x*i.m10+S*i.m20),M=qe(w*i.m01+x*i.m11+S*i.m21),p=qe(w*i.m02+x*i.m12+S*i.m22);return[R*255,M*255,p*255]},Me=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),[i,s,a]=_n(t,r,n),[c,u,f]=di(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]};function di(e,t,r){const{Xn:n,Yn:o,Zn:i,kE:s,kK:a}=ct,c=e/n,u=t/o,f=r/i,l=c>s?Math.pow(c,1/3):(a*c+16)/116,h=u>s?Math.pow(u,1/3):(a*u+16)/116,d=f>s?Math.pow(f,1/3):(a*f+16)/116;return[116*h-16,500*(l-h),200*(h-d)]}function Oe(e){const t=Math.sign(e);return e=Math.abs(e),(e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4))*t}const _n=(e,t,r)=>{e=Oe(e/255),t=Oe(t/255),r=Oe(r/255);const{MtxRGB2XYZ:n,MtxAdaptMa:o,MtxAdaptMaI:i,Xn:s,Yn:a,Zn:c,As:u,Bs:f,Cs:l}=ct;let h=e*n.m00+t*n.m10+r*n.m20,d=e*n.m01+t*n.m11+r*n.m21,b=e*n.m02+t*n.m12+r*n.m22;const g=s*o.m00+a*o.m10+c*o.m20,m=s*o.m01+a*o.m11+c*o.m21,y=s*o.m02+a*o.m12+c*o.m22;let L=h*o.m00+d*o.m10+b*o.m20,w=h*o.m01+d*o.m11+b*o.m21,x=h*o.m02+d*o.m12+b*o.m22;return L*=g/u,w*=m/f,x*=y/l,h=L*i.m00+w*i.m10+x*i.m20,d=L*i.m01+w*i.m11+x*i.m21,b=L*i.m02+w*i.m12+x*i.m22,[h,d,b]};_.prototype.lab=function(){return Me(this._rgb)},Object.assign(k,{lab:(...e)=>new _(...e,"lab"),getLabWhitePoint:Kt,setLabWhitePoint:ht}),$.format.lab=Re,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"lab"),N(e)==="array"&&e.length===3)return"lab"}}),_.prototype.darken=function(e=1){const t=this,r=t.lab();return r[0]-=ct.Kn*e,new _(r,"lab").alpha(t.alpha(),!0)},_.prototype.brighten=function(e=1){return this.darken(-e)},_.prototype.darker=_.prototype.darken,_.prototype.brighter=_.prototype.brighten,_.prototype.get=function(e){const[t,r]=e.split("."),n=this[t]();if(r){const o=t.indexOf(r)-(t.substr(0,2)==="ok"?2:0);if(o>-1)return n[o];throw new Error(`unknown channel ${r} in mode ${t}`)}else return n};const{pow:bi}=Math,pi=1e-7,mi=20;_.prototype.luminance=function(e,t="rgb"){if(e!==void 0&&N(e)==="number"){if(e===0)return new _([0,0,0,this._rgb[3]],"rgb");if(e===1)return new _([255,255,255,this._rgb[3]],"rgb");let r=this.luminance(),n=mi;const o=(s,a)=>{const c=s.interpolate(a,.5,t),u=c.luminance();return Math.abs(e-u)e?o(s,c):o(c,a)},i=(r>e?o(new _([0,0,0]),this):o(this,new _([255,255,255]))).rgb();return new _([...i,this._rgb[3]])}return gi(...this._rgb.slice(0,3))};const gi=(e,t,r)=>(e=Ae(e),t=Ae(t),r=Ae(r),.2126*e+.7152*t+.0722*r),Ae=e=>(e/=255,e<=.03928?e/12.92:bi((e+.055)/1.055,2.4)),H={},Nt=(e,t,r=.5,...n)=>{let o=n[0]||"lrgb";if(!H[o]&&!n.length&&(o=Object.keys(H)[0]),!H[o])throw new Error(`interpolation mode ${o} is not defined`);return N(e)!=="object"&&(e=new _(e)),N(t)!=="object"&&(t=new _(t)),H[o](e,t,r).alpha(e.alpha()+r*(t.alpha()-e.alpha()))};_.prototype.mix=_.prototype.interpolate=function(e,t=.5,...r){return Nt(this,e,t,...r)},_.prototype.premultiply=function(e=!1){const t=this._rgb,r=t[3];return e?(this._rgb=[t[0]*r,t[1]*r,t[2]*r,r],this):new _([t[0]*r,t[1]*r,t[2]*r,r],"rgb")};const{sin:_i,cos:vi}=Math,vn=(...e)=>{let[t,r,n]=E(e,"lch");return isNaN(n)&&(n=0),n=n*ii,[t,vi(n)*r,_i(n)*r]},Se=(...e)=>{e=E(e,"lch");const[t,r,n]=e,[o,i,s]=vn(t,r,n),[a,c,u]=Re(o,i,s);return[a,c,u,e.length>3?e[3]:1]},yi=(...e)=>{const t=bn(E(e,"hcl"));return Se(...t)},{sqrt:wi,atan2:xi,round:Ci}=Math,yn=(...e)=>{const[t,r,n]=E(e,"lab"),o=wi(r*r+n*n);let i=(xi(n,r)*ai+360)%360;return Ci(o*1e4)===0&&(i=Number.NaN),[t,o,i]},$e=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),[i,s,a]=Me(t,r,n),[c,u,f]=yn(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]};_.prototype.lch=function(){return $e(this._rgb)},_.prototype.hcl=function(){return bn($e(this._rgb))},Object.assign(k,{lch:(...e)=>new _(...e,"lch"),hcl:(...e)=>new _(...e,"hcl")}),$.format.lch=Se,$.format.hcl=yi,["lch","hcl"].forEach(e=>$.autodetect.push({p:2,test:(...t)=>{if(t=E(t,e),N(t)==="array"&&t.length===3)return e}})),_.prototype.saturate=function(e=1){const t=this,r=t.lch();return r[1]+=ct.Kn*e,r[1]<0&&(r[1]=0),new _(r,"lch").alpha(t.alpha(),!0)},_.prototype.desaturate=function(e=1){return this.saturate(-e)},_.prototype.set=function(e,t,r=!1){const[n,o]=e.split("."),i=this[n]();if(o){const s=n.indexOf(o)-(n.substr(0,2)==="ok"?2:0);if(s>-1){if(N(t)=="string")switch(t.charAt(0)){case"+":i[s]+=+t;break;case"-":i[s]+=+t;break;case"*":i[s]*=+t.substr(1);break;case"/":i[s]/=+t.substr(1);break;default:i[s]=+t}else if(N(t)==="number")i[s]=t;else throw new Error("unsupported value for Color.set");const a=new _(i,n);return r?(this._rgb=a._rgb,this):a}throw new Error(`unknown channel ${o} in mode ${n}`)}else return i},_.prototype.tint=function(e=.5,...t){return Nt(this,"white",e,...t)},_.prototype.shade=function(e=.5,...t){return Nt(this,"black",e,...t)};const ki=(e,t,r)=>{const n=e._rgb,o=t._rgb;return new _(n[0]+r*(o[0]-n[0]),n[1]+r*(o[1]-n[1]),n[2]+r*(o[2]-n[2]),"rgb")};H.rgb=ki;const{sqrt:Ee,pow:Tt}=Math,Ri=(e,t,r)=>{const[n,o,i]=e._rgb,[s,a,c]=t._rgb;return new _(Ee(Tt(n,2)*(1-r)+Tt(s,2)*r),Ee(Tt(o,2)*(1-r)+Tt(a,2)*r),Ee(Tt(i,2)*(1-r)+Tt(c,2)*r),"rgb")};H.lrgb=Ri;const qi=(e,t,r)=>{const n=e.lab(),o=t.lab();return new _(n[0]+r*(o[0]-n[0]),n[1]+r*(o[1]-n[1]),n[2]+r*(o[2]-n[2]),"lab")};H.lab=qi;const jt=(e,t,r,n)=>{let o,i;n==="hsl"?(o=e.hsl(),i=t.hsl()):n==="hsv"?(o=e.hsv(),i=t.hsv()):n==="hcg"?(o=e.hcg(),i=t.hcg()):n==="hsi"?(o=e.hsi(),i=t.hsi()):n==="lch"||n==="hcl"?(n="hcl",o=e.hcl(),i=t.hcl()):n==="oklch"&&(o=e.oklch().reverse(),i=t.oklch().reverse());let s,a,c,u,f,l;(n.substr(0,1)==="h"||n==="oklch")&&([s,c,f]=o,[a,u,l]=i);let h,d,b,g;return!isNaN(s)&&!isNaN(a)?(a>s&&a-s>180?g=a-(s+360):a180?g=a+360-s:g=a-s,d=s+r*g):isNaN(s)?isNaN(a)?d=Number.NaN:(d=a,(f==1||f==0)&&n!="hsv"&&(h=u)):(d=s,(l==1||l==0)&&n!="hsv"&&(h=c)),h===void 0&&(h=c+r*(u-c)),b=f+r*(l-f),n==="oklch"?new _([b,h,d],n):new _([d,h,b],n)},wn=(e,t,r)=>jt(e,t,r,"lch");H.lch=wn,H.hcl=wn;const Mi=e=>{if(N(e)=="number"&&e>=0&&e<=16777215){const t=e>>16,r=e>>8&255,n=e&255;return[t,r,n,1]}throw new Error("unknown num color: "+e)},Oi=(...e)=>{const[t,r,n]=E(e,"rgb");return(t<<16)+(r<<8)+n};_.prototype.num=function(){return Oi(this._rgb)},Object.assign(k,{num:(...e)=>new _(...e,"num")}),$.format.num=Mi,$.autodetect.push({p:5,test:(...e)=>{if(e.length===1&&N(e[0])==="number"&&e[0]>=0&&e[0]<=16777215)return"num"}});const Ai=(e,t,r)=>{const n=e.num(),o=t.num();return new _(n+r*(o-n),"num")};H.num=Ai;const{floor:Si}=Math,$i=(...e)=>{e=E(e,"hcg");let[t,r,n]=e,o,i,s;n=n*255;const a=r*255;if(r===0)o=i=s=n;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;const c=Si(t),u=t-c,f=n*(1-r),l=f+a*(1-u),h=f+a*u,d=f+a;switch(c){case 0:[o,i,s]=[d,h,f];break;case 1:[o,i,s]=[l,d,f];break;case 2:[o,i,s]=[f,d,h];break;case 3:[o,i,s]=[f,l,d];break;case 4:[o,i,s]=[h,f,d];break;case 5:[o,i,s]=[d,f,l];break}}return[o,i,s,e.length>3?e[3]:1]},Ei=(...e)=>{const[t,r,n]=E(e,"rgb"),o=hn(t,r,n),i=dn(t,r,n),s=i-o,a=s*100/255,c=o/(255-s)*100;let u;return s===0?u=Number.NaN:(t===i&&(u=(r-n)/s),r===i&&(u=2+(n-t)/s),n===i&&(u=4+(t-r)/s),u*=60,u<0&&(u+=360)),[u,a,c]};_.prototype.hcg=function(){return Ei(this._rgb)};const Li=(...e)=>new _(...e,"hcg");k.hcg=Li,$.format.hcg=$i,$.autodetect.push({p:1,test:(...e)=>{if(e=E(e,"hcg"),N(e)==="array"&&e.length===3)return"hcg"}});const Ni=(e,t,r)=>jt(e,t,r,"hcg");H.hcg=Ni;const{cos:Pt}=Math,Ti=(...e)=>{e=E(e,"hsi");let[t,r,n]=e,o,i,s;return isNaN(t)&&(t=0),isNaN(r)&&(r=0),t>360&&(t-=360),t<0&&(t+=360),t/=360,t<1/3?(s=(1-r)/3,o=(1+r*Pt(ft*t)/Pt(ke-ft*t))/3,i=1-(s+o)):t<2/3?(t-=1/3,o=(1-r)/3,i=(1+r*Pt(ft*t)/Pt(ke-ft*t))/3,s=1-(o+i)):(t-=2/3,i=(1-r)/3,s=(1+r*Pt(ft*t)/Pt(ke-ft*t))/3,o=1-(i+s)),o=qt(n*o*3),i=qt(n*i*3),s=qt(n*s*3),[o*255,i*255,s*255,e.length>3?e[3]:1]},{min:ji,sqrt:Pi,acos:zi}=Math,Bi=(...e)=>{let[t,r,n]=E(e,"rgb");t/=255,r/=255,n/=255;let o;const i=ji(t,r,n),s=(t+r+n)/3,a=s>0?1-i/s:0;return a===0?o=NaN:(o=(t-r+(t-n))/2,o/=Pi((t-r)*(t-r)+(t-n)*(r-n)),o=zi(o),n>r&&(o=ft-o),o/=ft),[o*360,a,s]};_.prototype.hsi=function(){return Bi(this._rgb)};const Ii=(...e)=>new _(...e,"hsi");k.hsi=Ii,$.format.hsi=Ti,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"hsi"),N(e)==="array"&&e.length===3)return"hsi"}});const Gi=(e,t,r)=>jt(e,t,r,"hsi");H.hsi=Gi;const Le=(...e)=>{e=E(e,"hsl");const[t,r,n]=e;let o,i,s;if(r===0)o=i=s=n*255;else{const a=[0,0,0],c=[0,0,0],u=n<.5?n*(1+r):n+r-n*r,f=2*n-u,l=t/360;a[0]=l+1/3,a[1]=l,a[2]=l-1/3;for(let h=0;h<3;h++)a[h]<0&&(a[h]+=1),a[h]>1&&(a[h]-=1),6*a[h]<1?c[h]=f+(u-f)*6*a[h]:2*a[h]<1?c[h]=u:3*a[h]<2?c[h]=f+(u-f)*(2/3-a[h])*6:c[h]=f;[o,i,s]=[c[0]*255,c[1]*255,c[2]*255]}return e.length>3?[o,i,s,e[3]]:[o,i,s,1]},xn=(...e)=>{e=E(e,"rgba");let[t,r,n]=e;t/=255,r/=255,n/=255;const o=hn(t,r,n),i=dn(t,r,n),s=(i+o)/2;let a,c;return i===o?(a=0,c=Number.NaN):a=s<.5?(i-o)/(i+o):(i-o)/(2-i-o),t==i?c=(r-n)/(i-o):r==i?c=2+(n-t)/(i-o):n==i&&(c=4+(t-r)/(i-o)),c*=60,c<0&&(c+=360),e.length>3&&e[3]!==void 0?[c,a,s,e[3]]:[c,a,s]};_.prototype.hsl=function(){return xn(this._rgb)};const Fi=(...e)=>new _(...e,"hsl");k.hsl=Fi,$.format.hsl=Le,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"hsl"),N(e)==="array"&&e.length===3)return"hsl"}});const Ki=(e,t,r)=>jt(e,t,r,"hsl");H.hsl=Ki;const{floor:Xi}=Math,Di=(...e)=>{e=E(e,"hsv");let[t,r,n]=e,o,i,s;if(n*=255,r===0)o=i=s=n;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;const a=Xi(t),c=t-a,u=n*(1-r),f=n*(1-r*c),l=n*(1-r*(1-c));switch(a){case 0:[o,i,s]=[n,l,u];break;case 1:[o,i,s]=[f,n,u];break;case 2:[o,i,s]=[u,n,l];break;case 3:[o,i,s]=[u,f,n];break;case 4:[o,i,s]=[l,u,n];break;case 5:[o,i,s]=[n,u,f];break}}return[o,i,s,e.length>3?e[3]:1]},{min:Vi,max:Yi}=Math,Zi=(...e)=>{e=E(e,"rgb");let[t,r,n]=e;const o=Vi(t,r,n),i=Yi(t,r,n),s=i-o;let a,c,u;return u=i/255,i===0?(a=Number.NaN,c=0):(c=s/i,t===i&&(a=(r-n)/s),r===i&&(a=2+(n-t)/s),n===i&&(a=4+(t-r)/s),a*=60,a<0&&(a+=360)),[a,c,u]};_.prototype.hsv=function(){return Zi(this._rgb)};const Ji=(...e)=>new _(...e,"hsv");k.hsv=Ji,$.format.hsv=Di,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"hsv"),N(e)==="array"&&e.length===3)return"hsv"}});const Hi=(e,t,r)=>jt(e,t,r,"hsv");H.hsv=Hi;function Qt(e,t){let r=e.length;Array.isArray(e[0])||(e=[e]),Array.isArray(t[0])||(t=t.map(s=>[s]));let n=t[0].length,o=t[0].map((s,a)=>t.map(c=>c[a])),i=e.map(s=>o.map(a=>Array.isArray(s)?s.reduce((c,u,f)=>c+u*(a[f]||0),0):a.reduce((c,u)=>c+u*s,0)));return r===1&&(i=i[0]),n===1?i.map(s=>s[0]):i}const Ne=(...e)=>{e=E(e,"lab");const[t,r,n,...o]=e,[i,s,a]=Wi([t,r,n]),[c,u,f]=gn(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]};function Wi(e){var t=[[1.2268798758459243,-.5578149944602171,.2813910456659647],[-.0405757452148008,1.112286803280317,-.0717110580655164],[-.0763729366746601,-.4214933324022432,1.5869240198367816]],r=[[1,.3963377773761749,.2158037573099136],[1,-.1055613458156586,-.0638541728258133],[1,-.0894841775298119,-1.2914855480194092]],n=Qt(r,e);return Qt(t,n.map(o=>o**3))}const Te=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),i=_n(t,r,n);return[...Ui(i),...o.length>0&&o[0]<1?[o[0]]:[]]};function Ui(e){const t=[[.819022437996703,.3619062600528904,-.1288737815209879],[.0329836539323885,.9292868615863434,.0361446663506424],[.0481771893596242,.2642395317527308,.6335478284694309]],r=[[.210454268309314,.7936177747023054,-.0040720430116193],[1.9779985324311684,-2.42859224204858,.450593709617411],[.0259040424655478,.7827717124575296,-.8086757549230774]],n=Qt(t,e);return Qt(r,n.map(o=>Math.cbrt(o)))}_.prototype.oklab=function(){return Te(this._rgb)},Object.assign(k,{oklab:(...e)=>new _(...e,"oklab")}),$.format.oklab=Ne,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"oklab"),N(e)==="array"&&e.length===3)return"oklab"}});const Qi=(e,t,r)=>{const n=e.oklab(),o=t.oklab();return new _(n[0]+r*(o[0]-n[0]),n[1]+r*(o[1]-n[1]),n[2]+r*(o[2]-n[2]),"oklab")};H.oklab=Qi;const ta=(e,t,r)=>jt(e,t,r,"oklch");H.oklch=ta;const{pow:je,sqrt:Pe,PI:ze,cos:Cn,sin:kn,atan2:ea}=Math,ra=(e,t="lrgb",r=null)=>{const n=e.length;r||(r=Array.from(new Array(n)).map(()=>1));const o=n/r.reduce(function(l,h){return l+h});if(r.forEach((l,h)=>{r[h]*=o}),e=e.map(l=>new _(l)),t==="lrgb")return na(e,r);const i=e.shift(),s=i.get(t),a=[];let c=0,u=0;for(let l=0;l{const d=l.get(t);f+=l.alpha()*r[h+1];for(let b=0;b=360;)h-=360;s[l]=h}else s[l]=s[l]/a[l];return f/=n,new _(s,t).alpha(f>.99999?1:f,!0)},na=(e,t)=>{const r=e.length,n=[0,0,0,0];for(let o=0;o.9999999&&(n[3]=1),new _(xe(n))},{pow:oa}=Math;function te(e){let t="rgb",r=k("#ccc"),n=0,o=[0,1],i=[0,1],s=[],a=[0,0],c=!1,u=[],f=!1,l=0,h=1,d=!1,b={},g=!0,m=1;const y=function(p){if(p=p||["#fff","#000"],p&&N(p)==="string"&&k.brewer&&k.brewer[p.toLowerCase()]&&(p=k.brewer[p.toLowerCase()]),N(p)==="array"){p.length===1&&(p=[p[0],p[0]]),p=p.slice(0);for(let C=0;C=c[O];)O++;return O-1}return 0};let w=p=>p,x=p=>p;const S=function(p,C){let O,q;if(C==null&&(C=!1),isNaN(p)||p===null)return r;C?q=p:c&&c.length>2?q=L(p)/(c.length-2):h!==l?q=(p-l)/(h-l):q=1,q=x(q),C||(q=w(q)),m!==1&&(q=oa(q,m)),q=a[0]+q*(1-a[0]-a[1]),q=qt(q,0,1);const T=Math.floor(q*1e4);if(g&&b[T])O=b[T];else{if(N(u)==="array")for(let A=0;A=P&&A===s.length-1){O=u[A];break}if(q>P&&qb={};y(e);const M=function(p){const C=k(S(p));return f&&C[f]?C[f]():C};return M.classes=function(p){if(p!=null){if(N(p)==="array")c=p,o=[p[0],p[p.length-1]];else{const C=k.analyze(o);p===0?c=[C.min,C.max]:c=k.limits(C,"e",p)}return M}return c},M.domain=function(p){if(!arguments.length)return i;i=p.slice(0),l=p[0],h=p[p.length-1],s=[];const C=u.length;if(p.length===C&&l!==h)for(let O of Array.from(p))s.push((O-l)/(h-l));else{for(let O=0;O2){const O=p.map((T,A)=>A/(p.length-1)),q=p.map(T=>(T-l)/(h-l));q.every((T,A)=>O[A]===T)||(x=T=>{if(T<=0||T>=1)return T;let A=0;for(;T>=q[A+1];)A++;const P=(T-q[A])/(q[A+1]-q[A]);return O[A]+P*(O[A+1]-O[A])})}}return o=[l,h],M},M.mode=function(p){return arguments.length?(t=p,R(),M):t},M.range=function(p,C){return y(p),M},M.out=function(p){return f=p,M},M.spread=function(p){return arguments.length?(n=p,M):n},M.correctLightness=function(p){return p==null&&(p=!0),d=p,R(),d?w=function(C){const O=S(0,!0).lab()[0],q=S(1,!0).lab()[0],T=O>q;let A=S(C,!0).lab()[0];const P=O+(q-O)*C;let G=A-P,Y=0,U=1,ut=20;for(;Math.abs(G)>.01&&ut-- >0;)(function(){return T&&(G*=-1),G<0?(Y=C,C+=(U-C)*.5):(U=C,C+=(Y-C)*.5),A=S(C,!0).lab()[0],G=A-P})();return C}:w=C=>C,M},M.padding=function(p){return p!=null?(N(p)==="number"&&(p=[p,p]),a=p,M):a},M.colors=function(p,C){arguments.length<2&&(C="hex");let O=[];if(arguments.length===0)O=u.slice(0);else if(p===1)O=[M(.5)];else if(p>1){const q=o[0],T=o[1]-q;O=sa(0,p).map(A=>M(q+A/(p-1)*T))}else{e=[];let q=[];if(c&&c.length>2)for(let T=1,A=c.length,P=1<=A;P?TA;P?T++:T--)q.push((c[T-1]+c[T])*.5);else q=o;O=q.map(T=>M(T))}return k[C]&&(O=O.map(q=>q[C]())),O},M.cache=function(p){return p!=null?(g=p,M):g},M.gamma=function(p){return p!=null?(m=p,M):m},M.nodata=function(p){return p!=null?(r=k(p),M):r},M}function sa(e,t,r){let n=[],o=ei;o?s++:s--)n.push(s);return n}const ia=function(e){let t=[1,1];for(let r=1;rnew _(i)),e.length===2)[r,n]=e.map(i=>i.lab()),t=function(i){const s=[0,1,2].map(a=>r[a]+i*(n[a]-r[a]));return new _(s,"lab")};else if(e.length===3)[r,n,o]=e.map(i=>i.lab()),t=function(i){const s=[0,1,2].map(a=>(1-i)*(1-i)*r[a]+2*(1-i)*i*n[a]+i*i*o[a]);return new _(s,"lab")};else if(e.length===4){let i;[r,n,o,i]=e.map(s=>s.lab()),t=function(s){const a=[0,1,2].map(c=>(1-s)*(1-s)*(1-s)*r[c]+3*(1-s)*(1-s)*s*n[c]+3*(1-s)*s*s*o[c]+s*s*s*i[c]);return new _(a,"lab")}}else if(e.length>=5){let i,s,a;i=e.map(c=>c.lab()),a=e.length-1,s=ia(a),t=function(c){const u=1-c,f=[0,1,2].map(l=>i.reduce((h,d,b)=>h+s[b]*u**(a-b)*c**b*d[l],0));return new _(f,"lab")}}else throw new RangeError("No point in running bezier with only one color.");return t},ca=e=>{const t=aa(e);return t.scale=()=>te(t),t},{round:Rn}=Math;_.prototype.rgb=function(e=!0){return e===!1?this._rgb.slice(0,3):this._rgb.slice(0,3).map(Rn)},_.prototype.rgba=function(e=!0){return this._rgb.slice(0,4).map((t,r)=>r<3?e===!1?t:Rn(t):t)},Object.assign(k,{rgb:(...e)=>new _(...e,"rgb")}),$.format.rgb=(...e)=>{const t=E(e,"rgba");return t[3]===void 0&&(t[3]=1),t},$.autodetect.push({p:3,test:(...e)=>{if(e=E(e,"rgba"),N(e)==="array"&&(e.length===3||e.length===4&&N(e[3])=="number"&&e[3]>=0&&e[3]<=1))return"rgb"}});const nt=(e,t,r)=>{if(!nt[r])throw new Error("unknown blend mode "+r);return nt[r](e,t)},_t=e=>(t,r)=>{const n=k(r).rgb(),o=k(t).rgb();return k.rgb(e(n,o))},vt=e=>(t,r)=>{const n=[];return n[0]=e(t[0],r[0]),n[1]=e(t[1],r[1]),n[2]=e(t[2],r[2]),n},ua=e=>e,la=(e,t)=>e*t/255,fa=(e,t)=>e>t?t:e,ha=(e,t)=>e>t?e:t,da=(e,t)=>255*(1-(1-e/255)*(1-t/255)),ba=(e,t)=>t<128?2*e*t/255:255*(1-2*(1-e/255)*(1-t/255)),pa=(e,t)=>255*(1-(1-t/255)/(e/255)),ma=(e,t)=>e===255?255:(e=255*(t/255)/(1-e/255),e>255?255:e);nt.normal=_t(vt(ua)),nt.multiply=_t(vt(la)),nt.screen=_t(vt(da)),nt.overlay=_t(vt(ba)),nt.darken=_t(vt(fa)),nt.lighten=_t(vt(ha)),nt.dodge=_t(vt(ma)),nt.burn=_t(vt(pa));const{pow:ga,sin:_a,cos:va}=Math;function ya(e=300,t=-1.5,r=1,n=1,o=[0,1]){let i=0,s;N(o)==="array"?s=o[1]-o[0]:(s=0,o=[o,o]);const a=function(c){const u=ft*((e+120)/360+t*c),f=ga(o[0]+s*c,n),h=(i!==0?r[0]+c*i:r)*f*(1-f)/2,d=va(u),b=_a(u),g=f+h*(-.14861*d+1.78277*b),m=f+h*(-.29227*d-.90649*b),y=f+h*(1.97294*d);return k(xe([g*255,m*255,y*255,1]))};return a.start=function(c){return c==null?e:(e=c,a)},a.rotations=function(c){return c==null?t:(t=c,a)},a.gamma=function(c){return c==null?n:(n=c,a)},a.hue=function(c){return c==null?r:(r=c,N(r)==="array"?(i=r[1]-r[0],i===0&&(r=r[1])):i=0,a)},a.lightness=function(c){return c==null?o:(N(c)==="array"?(o=c,s=c[1]-c[0]):(o=[c,c],s=0),a)},a.scale=()=>k.scale(a),a.hue(r),a}const wa="0123456789abcdef",{floor:xa,random:Ca}=Math,ka=(e=Ca)=>{let t="#";for(let r=0;r<6;r++)t+=wa.charAt(xa(e()*16));return new _(t,"hex")},{log:qn,pow:Ra,floor:qa,abs:Ma}=Math;function Mn(e,t=null){const r={min:Number.MAX_VALUE,max:Number.MAX_VALUE*-1,sum:0,values:[],count:0};return N(e)==="object"&&(e=Object.values(e)),e.forEach(n=>{t&&N(n)==="object"&&(n=n[t]),n!=null&&!isNaN(n)&&(r.values.push(n),r.sum+=n,nr.max&&(r.max=n),r.count+=1)}),r.domain=[r.min,r.max],r.limits=(n,o)=>On(r,n,o),r}function On(e,t="equal",r=7){N(e)=="array"&&(e=Mn(e));const{min:n,max:o}=e,i=e.values.sort((a,c)=>a-c);if(r===1)return[n,o];const s=[];if(t.substr(0,1)==="c"&&(s.push(n),s.push(o)),t.substr(0,1)==="e"){s.push(n);for(let a=1;a 0");const a=Math.LOG10E*qn(n),c=Math.LOG10E*qn(o);s.push(n);for(let u=1;u200&&(l=!1)}const b={};for(let m=0;mm-y),s.push(g[0]);for(let m=1;m{e=new _(e),t=new _(t);const r=e.luminance(),n=t.luminance();return r>n?(r+.05)/(n+.05):(n+.05)/(r+.05)};const An=.027,Aa=5e-4,Sa=.1,Sn=1.14,ee=.022,$n=1.414,$a=(e,t)=>{e=new _(e),t=new _(t),e.alpha()<1&&(e=Nt(t,e,e.alpha(),"rgb"));const r=En(...e.rgb()),n=En(...t.rgb()),o=r>=ee?r:r+Math.pow(ee-r,$n),i=n>=ee?n:n+Math.pow(ee-n,$n),s=Math.pow(i,.56)-Math.pow(o,.57),a=Math.pow(i,.65)-Math.pow(o,.62),c=Math.abs(i-o)0?c-An:c+An)*100};function En(e,t,r){return .2126729*Math.pow(e/255,2.4)+.7151522*Math.pow(t/255,2.4)+.072175*Math.pow(r/255,2.4)}const{sqrt:dt,pow:F,min:Ea,max:La,atan2:Ln,abs:Nn,cos:re,sin:Tn,exp:Na,PI:jn}=Math;function Ta(e,t,r=1,n=1,o=1){var i=function(Ct){return 360*Ct/(2*jn)},s=function(Ct){return 2*jn*Ct/360};e=new _(e),t=new _(t);const[a,c,u]=Array.from(e.lab()),[f,l,h]=Array.from(t.lab()),d=(a+f)/2,b=dt(F(c,2)+F(u,2)),g=dt(F(l,2)+F(h,2)),m=(b+g)/2,y=.5*(1-dt(F(m,7)/(F(m,7)+F(25,7)))),L=c*(1+y),w=l*(1+y),x=dt(F(L,2)+F(u,2)),S=dt(F(w,2)+F(h,2)),R=(x+S)/2,M=i(Ln(u,L)),p=i(Ln(h,w)),C=M>=0?M:M+360,O=p>=0?p:p+360,q=Nn(C-O)>180?(C+O+360)/2:(C+O)/2,T=1-.17*re(s(q-30))+.24*re(s(2*q))+.32*re(s(3*q+6))-.2*re(s(4*q-63));let A=O-C;A=Nn(A)<=180?A:O<=C?A+360:A-360,A=2*dt(x*S)*Tn(s(A)/2);const P=f-a,G=S-x,Y=1+.015*F(d-50,2)/dt(20+F(d-50,2)),U=1+.045*R,ut=1+.015*R*T,xt=30*Na(-F((q-275)/25,2)),lt=-(2*dt(F(R,7)/(F(R,7)+F(25,7))))*Tn(2*s(xt)),Mt=dt(F(P/(r*Y),2)+F(G/(n*U),2)+F(A/(o*ut),2)+lt*(G/(n*U))*(A/(o*ut)));return La(0,Ea(100,Mt))}function ja(e,t,r="lab"){e=new _(e),t=new _(t);const n=e.get(r),o=t.get(r);let i=0;for(let s in n){const a=(n[s]||0)-(o[s]||0);i+=a*a}return Math.sqrt(i)}const Pa=(...e)=>{try{return new _(...e),!0}catch{return!1}},za={cool(){return te([k.hsl(180,1,.9),k.hsl(250,.7,.4)])},hot(){return te(["#000","#f00","#ff0","#fff"]).mode("rgb")}},Be={OrRd:["#fff7ec","#fee8c8","#fdd49e","#fdbb84","#fc8d59","#ef6548","#d7301f","#b30000","#7f0000"],PuBu:["#fff7fb","#ece7f2","#d0d1e6","#a6bddb","#74a9cf","#3690c0","#0570b0","#045a8d","#023858"],BuPu:["#f7fcfd","#e0ecf4","#bfd3e6","#9ebcda","#8c96c6","#8c6bb1","#88419d","#810f7c","#4d004b"],Oranges:["#fff5eb","#fee6ce","#fdd0a2","#fdae6b","#fd8d3c","#f16913","#d94801","#a63603","#7f2704"],BuGn:["#f7fcfd","#e5f5f9","#ccece6","#99d8c9","#66c2a4","#41ae76","#238b45","#006d2c","#00441b"],YlOrBr:["#ffffe5","#fff7bc","#fee391","#fec44f","#fe9929","#ec7014","#cc4c02","#993404","#662506"],YlGn:["#ffffe5","#f7fcb9","#d9f0a3","#addd8e","#78c679","#41ab5d","#238443","#006837","#004529"],Reds:["#fff5f0","#fee0d2","#fcbba1","#fc9272","#fb6a4a","#ef3b2c","#cb181d","#a50f15","#67000d"],RdPu:["#fff7f3","#fde0dd","#fcc5c0","#fa9fb5","#f768a1","#dd3497","#ae017e","#7a0177","#49006a"],Greens:["#f7fcf5","#e5f5e0","#c7e9c0","#a1d99b","#74c476","#41ab5d","#238b45","#006d2c","#00441b"],YlGnBu:["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"],Purples:["#fcfbfd","#efedf5","#dadaeb","#bcbddc","#9e9ac8","#807dba","#6a51a3","#54278f","#3f007d"],GnBu:["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#0868ac","#084081"],Greys:["#ffffff","#f0f0f0","#d9d9d9","#bdbdbd","#969696","#737373","#525252","#252525","#000000"],YlOrRd:["#ffffcc","#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026","#800026"],PuRd:["#f7f4f9","#e7e1ef","#d4b9da","#c994c7","#df65b0","#e7298a","#ce1256","#980043","#67001f"],Blues:["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"],PuBuGn:["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"],Viridis:["#440154","#482777","#3f4a8a","#31678e","#26838f","#1f9d8a","#6cce5a","#b6de2b","#fee825"],Spectral:["#9e0142","#d53e4f","#f46d43","#fdae61","#fee08b","#ffffbf","#e6f598","#abdda4","#66c2a5","#3288bd","#5e4fa2"],RdYlGn:["#a50026","#d73027","#f46d43","#fdae61","#fee08b","#ffffbf","#d9ef8b","#a6d96a","#66bd63","#1a9850","#006837"],RdBu:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#f7f7f7","#d1e5f0","#92c5de","#4393c3","#2166ac","#053061"],PiYG:["#8e0152","#c51b7d","#de77ae","#f1b6da","#fde0ef","#f7f7f7","#e6f5d0","#b8e186","#7fbc41","#4d9221","#276419"],PRGn:["#40004b","#762a83","#9970ab","#c2a5cf","#e7d4e8","#f7f7f7","#d9f0d3","#a6dba0","#5aae61","#1b7837","#00441b"],RdYlBu:["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"],BrBG:["#543005","#8c510a","#bf812d","#dfc27d","#f6e8c3","#f5f5f5","#c7eae5","#80cdc1","#35978f","#01665e","#003c30"],RdGy:["#67001f","#b2182b","#d6604d","#f4a582","#fddbc7","#ffffff","#e0e0e0","#bababa","#878787","#4d4d4d","#1a1a1a"],PuOr:["#7f3b08","#b35806","#e08214","#fdb863","#fee0b6","#f7f7f7","#d8daeb","#b2abd2","#8073ac","#542788","#2d004b"],Set2:["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f","#e5c494","#b3b3b3"],Accent:["#7fc97f","#beaed4","#fdc086","#ffff99","#386cb0","#f0027f","#bf5b17","#666666"],Set1:["#e41a1c","#377eb8","#4daf4a","#984ea3","#ff7f00","#ffff33","#a65628","#f781bf","#999999"],Set3:["#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"],Dark2:["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"],Paired:["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928"],Pastel2:["#b3e2cd","#fdcdac","#cbd5e8","#f4cae4","#e6f5c9","#fff2ae","#f1e2cc","#cccccc"],Pastel1:["#fbb4ae","#b3cde3","#ccebc5","#decbe4","#fed9a6","#ffffcc","#e5d8bd","#fddaec","#f2f2f2"]},Pn=Object.keys(Be),zn=new Map(Pn.map(e=>[e.toLowerCase(),e])),Ba=typeof Proxy=="function"?new Proxy(Be,{get(e,t){const r=t.toLowerCase();if(zn.has(r))return e[zn.get(r)]},getOwnPropertyNames(){return Object.getOwnPropertyNames(Pn)}}):Be,Ia=(...e)=>{e=E(e,"cmyk");const[t,r,n,o]=e,i=e.length>4?e[4]:1;return o===1?[0,0,0,i]:[t>=1?0:255*(1-t)*(1-o),r>=1?0:255*(1-r)*(1-o),n>=1?0:255*(1-n)*(1-o),i]},{max:Bn}=Math,Ga=(...e)=>{let[t,r,n]=E(e,"rgb");t=t/255,r=r/255,n=n/255;const o=1-Bn(t,Bn(r,n)),i=o<1?1/(1-o):0,s=(1-t-o)*i,a=(1-r-o)*i,c=(1-n-o)*i;return[s,a,c,o]};_.prototype.cmyk=function(){return Ga(this._rgb)},Object.assign(k,{cmyk:(...e)=>new _(...e,"cmyk")}),$.format.cmyk=Ia,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"cmyk"),N(e)==="array"&&e.length===4)return"cmyk"}});const Fa=(...e)=>{const t=E(e,"hsla");let r=Et(e)||"lsa";return t[0]=et(t[0]||0)+"deg",t[1]=et(t[1]*100)+"%",t[2]=et(t[2]*100)+"%",r==="hsla"||t.length>3&&t[3]<1?(t[3]="/ "+(t.length>3?t[3]:1),r="hsla"):t.length=3,`${r.substr(0,3)}(${t.join(" ")})`},Ka=(...e)=>{const t=E(e,"lab");let r=Et(e)||"lab";return t[0]=et(t[0])+"%",t[1]=et(t[1]),t[2]=et(t[2]),r==="laba"||t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`lab(${t.join(" ")})`},Xa=(...e)=>{const t=E(e,"lch");let r=Et(e)||"lab";return t[0]=et(t[0])+"%",t[1]=et(t[1]),t[2]=isNaN(t[2])?"none":et(t[2])+"deg",r==="lcha"||t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`lch(${t.join(" ")})`},Da=(...e)=>{const t=E(e,"lab");return t[0]=et(t[0]*100)+"%",t[1]=Ce(t[1]),t[2]=Ce(t[2]),t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`oklab(${t.join(" ")})`},In=(...e)=>{const[t,r,n,...o]=E(e,"rgb"),[i,s,a]=Te(t,r,n),[c,u,f]=yn(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]},Va=(...e)=>{const t=E(e,"lch");return t[0]=et(t[0]*100)+"%",t[1]=Ce(t[1]),t[2]=isNaN(t[2])?"none":et(t[2])+"deg",t.length>3&&t[3]<1?t[3]="/ "+(t.length>3?t[3]:1):t.length=3,`oklch(${t.join(" ")})`},{round:Ie}=Math,Ya=(...e)=>{const t=E(e,"rgba");let r=Et(e)||"rgb";if(r.substr(0,3)==="hsl")return Fa(xn(t),r);if(r.substr(0,3)==="lab"){const n=Kt();ht("d50");const o=Ka(Me(t),r);return ht(n),o}if(r.substr(0,3)==="lch"){const n=Kt();ht("d50");const o=Xa($e(t),r);return ht(n),o}return r.substr(0,5)==="oklab"?Da(Te(t)):r.substr(0,5)==="oklch"?Va(In(t)):(t[0]=Ie(t[0]),t[1]=Ie(t[1]),t[2]=Ie(t[2]),(r==="rgba"||t.length>3&&t[3]<1)&&(t[3]="/ "+(t.length>3?t[3]:1),r="rgba"),`${r.substr(0,3)}(${t.slice(0,r==="rgb"?3:4).join(" ")})`)},Gn=(...e)=>{e=E(e,"lch");const[t,r,n,...o]=e,[i,s,a]=vn(t,r,n),[c,u,f]=Ne(i,s,a);return[c,u,f,...o.length>0&&o[0]<1?[o[0]]:[]]},bt=/((?:-?\d+)|(?:-?\d+(?:\.\d+)?)%|none)/.source,ot=/((?:-?(?:\d+(?:\.\d*)?|\.\d+)%?)|none)/.source,ne=/((?:-?(?:\d+(?:\.\d*)?|\.\d+)%)|none)/.source,rt=/\s*/.source,zt=/\s+/.source,Ge=/\s*,\s*/.source,oe=/((?:-?(?:\d+(?:\.\d*)?|\.\d+)(?:deg)?)|none)/.source,Bt=/\s*(?:\/\s*((?:[01]|[01]?\.\d+)|\d+(?:\.\d+)?%))?/.source,Fn=new RegExp("^rgba?\\("+rt+[bt,bt,bt].join(zt)+Bt+"\\)$"),Kn=new RegExp("^rgb\\("+rt+[bt,bt,bt].join(Ge)+rt+"\\)$"),Xn=new RegExp("^rgba\\("+rt+[bt,bt,bt,ot].join(Ge)+rt+"\\)$"),Dn=new RegExp("^hsla?\\("+rt+[oe,ne,ne].join(zt)+Bt+"\\)$"),Vn=new RegExp("^hsl?\\("+rt+[oe,ne,ne].join(Ge)+rt+"\\)$"),Yn=/^hsla\(\s*(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)%\s*,\s*(-?\d+(?:\.\d+)?)%\s*,\s*([01]|[01]?\.\d+)\)$/,Zn=new RegExp("^lab\\("+rt+[ot,ot,ot].join(zt)+Bt+"\\)$"),Jn=new RegExp("^lch\\("+rt+[ot,ot,oe].join(zt)+Bt+"\\)$"),Hn=new RegExp("^oklab\\("+rt+[ot,ot,ot].join(zt)+Bt+"\\)$"),Wn=new RegExp("^oklch\\("+rt+[ot,ot,oe].join(zt)+Bt+"\\)$"),{round:Un}=Math,It=e=>e.map((t,r)=>r<=2?qt(Un(t),0,255):t),K=(e,t=0,r=100,n=!1)=>(typeof e=="string"&&e.endsWith("%")&&(e=parseFloat(e.substring(0,e.length-1))/100,n?e=t+(e+1)*.5*(r-t):e=t+e*(r-t)),+e),W=(e,t)=>e==="none"?t:e,Fe=e=>{if(e=e.toLowerCase().trim(),e==="transparent")return[0,0,0,0];let t;if($.format.named)try{return $.format.named(e)}catch{}if((t=e.match(Fn))||(t=e.match(Kn))){let r=t.slice(1,4);for(let o=0;o<3;o++)r[o]=+K(W(r[o],0),0,255);r=It(r);const n=t[4]!==void 0?+K(t[4],0,1):1;return r[3]=n,r}if(t=e.match(Xn)){const r=t.slice(1,5);for(let n=0;n<4;n++)r[n]=+K(r[n],0,255);return r}if((t=e.match(Dn))||(t=e.match(Vn))){const r=t.slice(1,4);r[0]=+W(r[0].replace("deg",""),0),r[1]=+K(W(r[1],0),0,100)*.01,r[2]=+K(W(r[2],0),0,100)*.01;const n=It(Le(r)),o=t[4]!==void 0?+K(t[4],0,1):1;return n[3]=o,n}if(t=e.match(Yn)){const r=t.slice(1,4);r[1]*=.01,r[2]*=.01;const n=Le(r);for(let o=0;o<3;o++)n[o]=Un(n[o]);return n[3]=+t[4],n}if(t=e.match(Zn)){const r=t.slice(1,4);r[0]=K(W(r[0],0),0,100),r[1]=K(W(r[1],0),-125,125,!0),r[2]=K(W(r[2],0),-125,125,!0);const n=Kt();ht("d50");const o=It(Re(r));ht(n);const i=t[4]!==void 0?+K(t[4],0,1):1;return o[3]=i,o}if(t=e.match(Jn)){const r=t.slice(1,4);r[0]=K(r[0],0,100),r[1]=K(W(r[1],0),0,150,!1),r[2]=+W(r[2].replace("deg",""),0);const n=Kt();ht("d50");const o=It(Se(r));ht(n);const i=t[4]!==void 0?+K(t[4],0,1):1;return o[3]=i,o}if(t=e.match(Hn)){const r=t.slice(1,4);r[0]=K(W(r[0],0),0,1),r[1]=K(W(r[1],0),-.4,.4,!0),r[2]=K(W(r[2],0),-.4,.4,!0);const n=It(Ne(r)),o=t[4]!==void 0?+K(t[4],0,1):1;return n[3]=o,n}if(t=e.match(Wn)){const r=t.slice(1,4);r[0]=K(W(r[0],0),0,1),r[1]=K(W(r[1],0),0,.4,!1),r[2]=+W(r[2].replace("deg",""),0);const n=It(Gn(r)),o=t[4]!==void 0?+K(t[4],0,1):1;return n[3]=o,n}};Fe.test=e=>Fn.test(e)||Dn.test(e)||Zn.test(e)||Jn.test(e)||Hn.test(e)||Wn.test(e)||Kn.test(e)||Xn.test(e)||Vn.test(e)||Yn.test(e)||e==="transparent",_.prototype.css=function(e){return Ya(this._rgb,e)};const Za=(...e)=>new _(...e,"css");k.css=Za,$.format.css=Fe,$.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&N(e)==="string"&&Fe.test(e))return"css"}}),$.format.gl=(...e)=>{const t=E(e,"rgba");return t[0]*=255,t[1]*=255,t[2]*=255,t};const Ja=(...e)=>new _(...e,"gl");k.gl=Ja,_.prototype.gl=function(){const e=this._rgb;return[e[0]/255,e[1]/255,e[2]/255,e[3]]},_.prototype.hex=function(e){return mn(this._rgb,e)};const Ha=(...e)=>new _(...e,"hex");k.hex=Ha,$.format.hex=pn,$.autodetect.push({p:4,test:(e,...t)=>{if(!t.length&&N(e)==="string"&&[3,4,5,6,7,8,9].indexOf(e.length)>=0)return"hex"}});const{log:se}=Math,Qn=e=>{const t=e/100;let r,n,o;return t<66?(r=255,n=t<6?0:-155.25485562709179-.44596950469579133*(n=t-2)+104.49216199393888*se(n),o=t<20?0:-254.76935184120902+.8274096064007395*(o=t-10)+115.67994401066147*se(o)):(r=351.97690566805693+.114206453784165*(r=t-55)-40.25366309332127*se(r),n=325.4494125711974+.07943456536662342*(n=t-50)-28.0852963507957*se(n),o=255),[r,n,o,1]},{round:Wa}=Math,Ua=(...e)=>{const t=E(e,"rgb"),r=t[0],n=t[2];let o=1e3,i=4e4;const s=.4;let a;for(;i-o>s;){a=(i+o)*.5;const c=Qn(a);c[2]/c[0]>=n/r?i=a:o=a}return Wa(a)};_.prototype.temp=_.prototype.kelvin=_.prototype.temperature=function(){return Ua(this._rgb)};const Ke=(...e)=>new _(...e,"temp");Object.assign(k,{temp:Ke,kelvin:Ke,temperature:Ke}),$.format.temp=$.format.kelvin=$.format.temperature=Qn,_.prototype.oklch=function(){return In(this._rgb)},Object.assign(k,{oklch:(...e)=>new _(...e,"oklch")}),$.format.oklch=Gn,$.autodetect.push({p:2,test:(...e)=>{if(e=E(e,"oklch"),N(e)==="array"&&e.length===3)return"oklch"}}),Object.assign(k,{analyze:Mn,average:ra,bezier:ca,blend:nt,brewer:Ba,Color:_,colors:Lt,contrast:Oa,contrastAPCA:$a,cubehelix:ya,deltaE:Ta,distance:ja,input:$,interpolate:Nt,limits:On,mix:Nt,random:ka,scale:te,scales:za,valid:Pa});class v{constructor(){this.hex="#000000",this.rgb_r=0,this.rgb_g=0,this.rgb_b=0,this.xyz_x=0,this.xyz_y=0,this.xyz_z=0,this.luv_l=0,this.luv_u=0,this.luv_v=0,this.lch_l=0,this.lch_c=0,this.lch_h=0,this.hsluv_h=0,this.hsluv_s=0,this.hsluv_l=0,this.hpluv_h=0,this.hpluv_p=0,this.hpluv_l=0,this.r0s=0,this.r0i=0,this.r1s=0,this.r1i=0,this.g0s=0,this.g0i=0,this.g1s=0,this.g1i=0,this.b0s=0,this.b0i=0,this.b1s=0,this.b1i=0}static fromLinear(t){return t<=.0031308?12.92*t:1.055*Math.pow(t,.4166666666666667)-.055}static toLinear(t){return t>.04045?Math.pow((t+.055)/1.055,2.4):t/12.92}static yToL(t){return t<=v.epsilon?t/v.refY*v.kappa:116*Math.pow(t/v.refY,.3333333333333333)-16}static lToY(t){return t<=8?v.refY*t/v.kappa:v.refY*Math.pow((t+16)/116,3)}static rgbChannelToHex(t){const r=Math.round(t*255),n=r%16,o=(r-n)/16|0;return v.hexChars.charAt(o)+v.hexChars.charAt(n)}static hexToRgbChannel(t,r){const n=v.hexChars.indexOf(t.charAt(r)),o=v.hexChars.indexOf(t.charAt(r+1));return(n*16+o)/255}static distanceFromOriginAngle(t,r,n){const o=r/(Math.sin(n)-t*Math.cos(n));return o<0?1/0:o}static distanceFromOrigin(t,r){return Math.abs(r)/Math.sqrt(Math.pow(t,2)+1)}static min6(t,r,n,o,i,s){return Math.min(t,Math.min(r,Math.min(n,Math.min(o,Math.min(i,s)))))}rgbToHex(){this.hex="#",this.hex+=v.rgbChannelToHex(this.rgb_r),this.hex+=v.rgbChannelToHex(this.rgb_g),this.hex+=v.rgbChannelToHex(this.rgb_b)}hexToRgb(){this.hex=this.hex.toLowerCase(),this.rgb_r=v.hexToRgbChannel(this.hex,1),this.rgb_g=v.hexToRgbChannel(this.hex,3),this.rgb_b=v.hexToRgbChannel(this.hex,5)}xyzToRgb(){this.rgb_r=v.fromLinear(v.m_r0*this.xyz_x+v.m_r1*this.xyz_y+v.m_r2*this.xyz_z),this.rgb_g=v.fromLinear(v.m_g0*this.xyz_x+v.m_g1*this.xyz_y+v.m_g2*this.xyz_z),this.rgb_b=v.fromLinear(v.m_b0*this.xyz_x+v.m_b1*this.xyz_y+v.m_b2*this.xyz_z)}rgbToXyz(){const t=v.toLinear(this.rgb_r),r=v.toLinear(this.rgb_g),n=v.toLinear(this.rgb_b);this.xyz_x=.41239079926595*t+.35758433938387*r+.18048078840183*n,this.xyz_y=.21263900587151*t+.71516867876775*r+.072192315360733*n,this.xyz_z=.019330818715591*t+.11919477979462*r+.95053215224966*n}xyzToLuv(){const t=this.xyz_x+15*this.xyz_y+3*this.xyz_z;let r=4*this.xyz_x,n=9*this.xyz_y;t!==0?(r/=t,n/=t):(r=NaN,n=NaN),this.luv_l=v.yToL(this.xyz_y),this.luv_l===0?(this.luv_u=0,this.luv_v=0):(this.luv_u=13*this.luv_l*(r-v.refU),this.luv_v=13*this.luv_l*(n-v.refV))}luvToXyz(){if(this.luv_l===0){this.xyz_x=0,this.xyz_y=0,this.xyz_z=0;return}const t=this.luv_u/(13*this.luv_l)+v.refU,r=this.luv_v/(13*this.luv_l)+v.refV;this.xyz_y=v.lToY(this.luv_l),this.xyz_x=0-9*this.xyz_y*t/((t-4)*r-t*r),this.xyz_z=(9*this.xyz_y-15*r*this.xyz_y-r*this.xyz_x)/(3*r)}luvToLch(){if(this.lch_l=this.luv_l,this.lch_c=Math.sqrt(this.luv_u*this.luv_u+this.luv_v*this.luv_v),this.lch_c<1e-8)this.lch_h=0;else{const t=Math.atan2(this.luv_v,this.luv_u);this.lch_h=t*180/Math.PI,this.lch_h<0&&(this.lch_h=360+this.lch_h)}}lchToLuv(){const t=this.lch_h/180*Math.PI;this.luv_l=this.lch_l,this.luv_u=Math.cos(t)*this.lch_c,this.luv_v=Math.sin(t)*this.lch_c}calculateBoundingLines(t){const r=Math.pow(t+16,3)/1560896,n=r>v.epsilon?r:t/v.kappa,o=n*(284517*v.m_r0-94839*v.m_r2),i=n*(838422*v.m_r2+769860*v.m_r1+731718*v.m_r0),s=n*(632260*v.m_r2-126452*v.m_r1),a=n*(284517*v.m_g0-94839*v.m_g2),c=n*(838422*v.m_g2+769860*v.m_g1+731718*v.m_g0),u=n*(632260*v.m_g2-126452*v.m_g1),f=n*(284517*v.m_b0-94839*v.m_b2),l=n*(838422*v.m_b2+769860*v.m_b1+731718*v.m_b0),h=n*(632260*v.m_b2-126452*v.m_b1);this.r0s=o/s,this.r0i=i*t/s,this.r1s=o/(s+126452),this.r1i=(i-769860)*t/(s+126452),this.g0s=a/u,this.g0i=c*t/u,this.g1s=a/(u+126452),this.g1i=(c-769860)*t/(u+126452),this.b0s=f/h,this.b0i=l*t/h,this.b1s=f/(h+126452),this.b1i=(l-769860)*t/(h+126452)}calcMaxChromaHpluv(){const t=v.distanceFromOrigin(this.r0s,this.r0i),r=v.distanceFromOrigin(this.r1s,this.r1i),n=v.distanceFromOrigin(this.g0s,this.g0i),o=v.distanceFromOrigin(this.g1s,this.g1i),i=v.distanceFromOrigin(this.b0s,this.b0i),s=v.distanceFromOrigin(this.b1s,this.b1i);return v.min6(t,r,n,o,i,s)}calcMaxChromaHsluv(t){const r=t/360*Math.PI*2,n=v.distanceFromOriginAngle(this.r0s,this.r0i,r),o=v.distanceFromOriginAngle(this.r1s,this.r1i,r),i=v.distanceFromOriginAngle(this.g0s,this.g0i,r),s=v.distanceFromOriginAngle(this.g1s,this.g1i,r),a=v.distanceFromOriginAngle(this.b0s,this.b0i,r),c=v.distanceFromOriginAngle(this.b1s,this.b1i,r);return v.min6(n,o,i,s,a,c)}hsluvToLch(){if(this.hsluv_l>99.9999999)this.lch_l=100,this.lch_c=0;else if(this.hsluv_l<1e-8)this.lch_l=0,this.lch_c=0;else{this.lch_l=this.hsluv_l,this.calculateBoundingLines(this.hsluv_l);const t=this.calcMaxChromaHsluv(this.hsluv_h);this.lch_c=t/100*this.hsluv_s}this.lch_h=this.hsluv_h}lchToHsluv(){if(this.lch_l>99.9999999)this.hsluv_s=0,this.hsluv_l=100;else if(this.lch_l<1e-8)this.hsluv_s=0,this.hsluv_l=0;else{this.calculateBoundingLines(this.lch_l);const t=this.calcMaxChromaHsluv(this.lch_h);this.hsluv_s=this.lch_c/t*100,this.hsluv_l=this.lch_l}this.hsluv_h=this.lch_h}hpluvToLch(){if(this.hpluv_l>99.9999999)this.lch_l=100,this.lch_c=0;else if(this.hpluv_l<1e-8)this.lch_l=0,this.lch_c=0;else{this.lch_l=this.hpluv_l,this.calculateBoundingLines(this.hpluv_l);const t=this.calcMaxChromaHpluv();this.lch_c=t/100*this.hpluv_p}this.lch_h=this.hpluv_h}lchToHpluv(){if(this.lch_l>99.9999999)this.hpluv_p=0,this.hpluv_l=100;else if(this.lch_l<1e-8)this.hpluv_p=0,this.hpluv_l=0;else{this.calculateBoundingLines(this.lch_l);const t=this.calcMaxChromaHpluv();this.hpluv_p=this.lch_c/t*100,this.hpluv_l=this.lch_l}this.hpluv_h=this.lch_h}hsluvToRgb(){this.hsluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hpluvToRgb(){this.hpluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hsluvToHex(){this.hsluvToRgb(),this.rgbToHex()}hpluvToHex(){this.hpluvToRgb(),this.rgbToHex()}rgbToHsluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHsluv()}rgbToHpluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHpluv()}hexToHsluv(){this.hexToRgb(),this.rgbToHsluv()}hexToHpluv(){this.hexToRgb(),this.rgbToHpluv()}}v.hexChars="0123456789abcdef",v.refY=1,v.refU=.19783000664283,v.refV=.46831999493879,v.kappa=903.2962962,v.epsilon=.0088564516,v.m_r0=3.240969941904521,v.m_r1=-1.537383177570093,v.m_r2=-.498610760293,v.m_g0=-.96924363628087,v.m_g1=1.87596750150772,v.m_g2=.041555057407175,v.m_b0=.055630079696993,v.m_b1=-.20397695888897,v.m_b2=1.056971514242878;function to(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ie={exports:{}},Xe,eo;function Xt(){if(eo)return Xe;eo=1;function e(t,r){return Object.prototype.hasOwnProperty.call(t,r)}return Xe=e,Xe}var De,ro;function Ve(){if(ro)return De;ro=1;var e=Xt(),t,r;function n(){r=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],t=!0;for(var s in{toString:null})t=!1}function o(s,a,c){var u,f=0;t==null&&n();for(u in s)if(i(a,s,u,c)===!1)break;if(t)for(var l=s.constructor,h=!!l&&s===l.prototype;(u=r[f++])&&!((u!=="constructor"||!h&&e(s,u))&&s[u]!==Object.prototype[u]&&i(a,s,u,c)===!1););}function i(s,a,c,u){return s.call(u,a[c],c,a)}return De=o,De}var Ye,no;function oo(){if(no)return Ye;no=1;var e=Ve();function t(r){var n=[];return e(r,function(o,i){typeof o=="function"&&n.push(i)}),n.sort()}return Ye=t,Ye}var Ze,so;function Dt(){if(so)return Ze;so=1;function e(t,r,n){var o=t.length;r==null?r=0:r<0?r=Math.max(o+r,0):r=Math.min(r,o),n==null?n=o:n<0?n=Math.max(o+n,0):n=Math.min(n,o);for(var i=[];r1?n(arguments,1):e(i);r(a,function(c){i[c]=t(i[c],i)})}return Ue=o,Ue}var Qe,uo;function V(){if(uo)return Qe;uo=1;var e=Xt(),t=Ve();function r(n,o,i){t(n,function(s,a){if(e(n,a))return o.call(i,n[a],a,n)})}return Qe=r,Qe}var tr,lo;function ec(){if(lo)return tr;lo=1;function e(t){return t}return tr=e,tr}var er,fo;function ho(){if(fo)return er;fo=1;function e(t){return function(r){return r[t]}}return er=e,er}var rr,bo;function nr(){if(bo)return rr;bo=1;var e=/^\[object (.*)\]$/,t=Object.prototype.toString,r;function n(o){return o===null?"Null":o===r?"Undefined":e.exec(t.call(o))[1]}return rr=n,rr}var or,po;function sr(){if(po)return or;po=1;var e=nr();function t(r,n){return e(r)===n}return or=t,or}var ir,mo;function rc(){if(mo)return ir;mo=1;var e=sr(),t=Array.isArray||function(r){return e(r,"Array")};return ir=t,ir}var ar,go;function _o(){if(go)return ar;go=1;var e=V(),t=rc();function r(s,a){for(var c=-1,u=s.length;++cs&&(s=c,i=a);return i}return Or=t,Or}var Ar,Do;function Sr(){if(Do)return Ar;Do=1;var e=V();function t(r){var n=[];return e(r,function(o,i){n.push(o)}),n}return Ar=t,Ar}var $r,Vo;function bc(){if(Vo)return $r;Vo=1;var e=dc(),t=Sr();function r(n,o){return e(t(n),o)}return $r=r,$r}var Er,Yo;function Zo(){if(Yo)return Er;Yo=1;var e=V();function t(n,o){for(var i=0,s=arguments.length,a;++i2;if(!t(n)&&!a)throw new Error("reduce of empty object with no initial value");return e(n,function(c,u,f){a?i=o.call(s,i,c,u,f):(i=c,a=!0)}),i}return Dr=r,Dr}var Vr,ls;function qc(){if(ls)return Vr;ls=1;var e=Lo(),t=yt();function r(n,o,i){return o=t(o,i),e(n,function(s,a,c){return!o(s,a,c)},i)}return Vr=r,Vr}var Yr,fs;function Mc(){if(fs)return Yr;fs=1;var e=sr();function t(r){return e(r,"Function")}return Yr=t,Yr}var Zr,hs;function Oc(){if(hs)return Zr;hs=1;var e=Mc();function t(r,n){var o=r[n];if(o!==void 0)return e(o)?o.call(r):o}return Zr=t,Zr}var Jr,ds;function Ac(){if(ds)return Jr;ds=1;var e=es();function t(r,n,o){var i=/^(.+)\.(.+)$/.exec(n);i?e(r,i[1])[i[2]]=o:r[n]=o}return Jr=t,Jr}var Hr,bs;function Sc(){if(bs)return Hr;bs=1;var e=Bo();function t(r,n){if(e(r,n)){for(var o=n.split("."),i=o.pop();n=o.shift();)r=r[n];return delete r[i]}else return!0}return Hr=t,Hr}var Wr,ps;function Ur(){return ps||(ps=1,Wr={bindAll:tc(),contains:nc(),deepFillIn:oc(),deepMatches:_o(),deepMixIn:sc(),equals:ac(),every:qo(),fillIn:cc(),filter:Lo(),find:uc(),flatten:lc(),forIn:Ve(),forOwn:V(),functions:oo(),get:Po(),has:Bo(),hasOwn:Xt(),keys:fc(),map:Fo(),matches:hc(),max:bc(),merge:gc(),min:vc(),mixIn:Zo(),namespace:es(),omit:xc(),pick:Cc(),pluck:kc(),reduce:Rc(),reject:qc(),result:Oc(),set:Ac(),size:cs(),some:lr(),unset:Sc(),values:Sr()}),Wr}var ms;function gs(){return ms||(ms=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=Ur(),n={A:{x:.44758,y:.40745},C:{x:.31006,y:.31616},D50:{x:.34567,y:.35851},D65:{x:.31272,y:.32903},D55:{x:.33243,y:.34744},D75:{x:.29903,y:.31488}},o=(0,r.map)(n,function(i){var s=100*(i.x/i.y),a=100,c=100*(1-i.x-i.y)/i.y;return[s,a,c]});t.default=o,e.exports=t.default})(ie,ie.exports)),ie.exports}var ae={exports:{}},_s;function vs(){return _s||(_s=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=Math,n=r.pow,o=r.sign,i=r.abs,s={decode:function(l){return l<=.04045?l/12.92:n((l+.055)/1.055,2.4)},encode:function(l){return l<=.0031308?12.92*l:1.055*n(l,1/2.4)-.055}},a={encode:function(l){return l<.001953125?16*l:n(l,1/1.8)},decode:function(l){return l<16*.001953125?l/16:n(l,1.8)}};function c(f){return{decode:function(h){return o(h)*n(i(h),f)},encode:function(h){return o(h)*n(i(h),1/f)}}}var u={sRGB:{r:{x:.64,y:.33},g:{x:.3,y:.6},b:{x:.15,y:.06},gamma:s},"Adobe RGB":{r:{x:.64,y:.33},g:{x:.21,y:.71},b:{x:.15,y:.06},gamma:c(2.2)},"Wide Gamut RGB":{r:{x:.7347,y:.2653},g:{x:.1152,y:.8264},b:{x:.1566,y:.0177},gamma:c(563/256)},"ProPhoto RGB":{r:{x:.7347,y:.2653},g:{x:.1596,y:.8404},b:{x:.0366,y:1e-4},gamma:a}};t.default=u,e.exports=t.default})(ae,ae.exports)),ae.exports}var pt={},ys;function ws(){if(ys)return pt;ys=1,Object.defineProperty(pt,"__esModule",{value:!0});function e(s){return[[s[0][0],s[1][0],s[2][0]],[s[0][1],s[1][1],s[2][1]],[s[0][2],s[1][2],s[2][2]]]}function t(s){return s[0][0]*(s[2][2]*s[1][1]-s[2][1]*s[1][2])+s[1][0]*(s[2][1]*s[0][2]-s[2][2]*s[0][1])+s[2][0]*(s[1][2]*s[0][1]-s[1][1]*s[0][2])}function r(s){var a=1/t(s);return[[(s[2][2]*s[1][1]-s[2][1]*s[1][2])*a,(s[2][1]*s[0][2]-s[2][2]*s[0][1])*a,(s[1][2]*s[0][1]-s[1][1]*s[0][2])*a],[(s[2][0]*s[1][2]-s[2][2]*s[1][0])*a,(s[2][2]*s[0][0]-s[2][0]*s[0][2])*a,(s[1][0]*s[0][2]-s[1][2]*s[0][0])*a],[(s[2][1]*s[1][0]-s[2][0]*s[1][1])*a,(s[2][0]*s[0][1]-s[2][1]*s[0][0])*a,(s[1][1]*s[0][0]-s[1][0]*s[0][1])*a]]}function n(s,a){return[s[0][0]*a[0]+s[0][1]*a[1]+s[0][2]*a[2],s[1][0]*a[0]+s[1][1]*a[1]+s[1][2]*a[2],s[2][0]*a[0]+s[2][1]*a[1]+s[2][2]*a[2]]}function o(s,a){return[[s[0][0]*a[0],s[0][1]*a[1],s[0][2]*a[2]],[s[1][0]*a[0],s[1][1]*a[1],s[1][2]*a[2]],[s[2][0]*a[0],s[2][1]*a[1],s[2][2]*a[2]]]}function i(s,a){return[[s[0][0]*a[0][0]+s[0][1]*a[1][0]+s[0][2]*a[2][0],s[0][0]*a[0][1]+s[0][1]*a[1][1]+s[0][2]*a[2][1],s[0][0]*a[0][2]+s[0][1]*a[1][2]+s[0][2]*a[2][2]],[s[1][0]*a[0][0]+s[1][1]*a[1][0]+s[1][2]*a[2][0],s[1][0]*a[0][1]+s[1][1]*a[1][1]+s[1][2]*a[2][1],s[1][0]*a[0][2]+s[1][1]*a[1][2]+s[1][2]*a[2][2]],[s[2][0]*a[0][0]+s[2][1]*a[1][0]+s[2][2]*a[2][0],s[2][0]*a[0][1]+s[2][1]*a[1][1]+s[2][2]*a[2][1],s[2][0]*a[0][2]+s[2][1]*a[1][2]+s[2][2]*a[2][2]]]}return pt.transpose=e,pt.determinant=t,pt.inverse=r,pt.multiply=n,pt.scalar=o,pt.product=i,pt}var Yt={},xs;function $c(){if(xs)return Yt;xs=1,Object.defineProperty(Yt,"__esModule",{value:!0});var e=Math,t=e.PI;function r(o){for(var i=o*180/t;i<0;)i+=360;for(;i>360;)i-=360;return i}function n(o){for(var i=t*o/180;i<0;)i+=2*t;for(;i>2*t;)i-=2*t;return i}return Yt.fromRadian=r,Yt.toRadian=n,Yt}var Zt={},Cs;function Ec(){if(Cs)return Zt;Cs=1,Object.defineProperty(Zt,"__esModule",{value:!0});var e=Math,t=e.round;function r(o){return o[0]=="#"&&(o=o.slice(1)),o.length<6&&(o=o.split("").map(function(i){return i+i}).join("")),o.match(/../g).map(function(i){return parseInt(i,16)/255})}function n(o){var i=o.map(function(s){return s=t(255*s).toString(16),s.length<2&&(s="0"+s),s}).join("");return"#"+i}return Zt.fromHex=r,Zt.toHex=n,Zt}var ce={exports:{}},ks;function Lc(){return ks||(ks=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=ws(),n=u(r),o=gs(),i=c(o),s=vs(),a=c(s);function c(l){return l&&l.__esModule?l:{default:l}}function u(l){if(l&&l.__esModule)return l;var h={};if(l!=null)for(var d in l)Object.prototype.hasOwnProperty.call(l,d)&&(h[d]=l[d]);return h.default=l,h}function f(){var l=arguments.length<=0||arguments[0]===void 0?a.default.sRGB:arguments[0],h=arguments.length<=1||arguments[1]===void 0?i.default.D65:arguments[1],d=[l.r,l.g,l.b],b=n.transpose(d.map(function(w){var x=w.x/w.y,S=1,R=(1-w.x-w.y)/w.y;return[x,S,R]})),g=l.gamma,m=n.multiply(n.inverse(b),h),y=n.scalar(b,m),L=n.inverse(y);return{fromRgb:function(x){return n.multiply(y,x.map(g.decode))},toRgb:function(x){return n.multiply(L,x).map(g.encode)}}}t.default=f,e.exports=t.default})(ce,ce.exports)),ce.exports}var Qr,Rs;function ue(){if(Rs)return Qr;Rs=1;var e=gs(),t=vs(),r=ws(),n=$c(),o=Ec(),i=Lc();return Qr={illuminant:e,workspace:t,matrix:r,degree:n,rgb:o,xyz:i},Qr}var Nc=ue();const le=to(Nc);var st={},qs;function fe(){if(qs)return st;qs=1,Object.defineProperty(st,"__esModule",{value:!0}),st.cfs=st.distance=st.lerp=st.corLerp=void 0;var e=Ur();function t(h,d,b){return d in h?Object.defineProperty(h,d,{value:b,enumerable:!0,configurable:!0,writable:!0}):h[d]=b,h}function r(h){if(Array.isArray(h)){for(var d=0,b=Array(h.length);dm/2&&(h>d?d+=m:h+=m)}return((1-b)*h+b*d)%(m||1/0)}function u(h,d,b){var g={};for(var m in h)g[m]=c(h[m],d[m],b,m);return g}function f(h,d){var b=0;for(var g in h)b+=i(h[g]-d[g],2);return s(b)}function l(h){return e.merge.apply(void 0,r(h.split("").map(function(d){return t({},d,!0)})))}return st.corLerp=c,st.lerp=u,st.distance=f,st.cfs=l,st}var he={exports:{}},Ms;function Tc(){return Ms||(Ms=1,(function(e,t){var r=(function(){function s(a,c){var u=[],f=!0,l=!1,h=void 0;try{for(var d=a[Symbol.iterator](),b;!(f=(b=d.next()).done)&&(u.push(b.value),!(c&&u.length===c));f=!0);}catch(g){l=!0,h=g}finally{try{!f&&d.return&&d.return()}finally{if(l)throw h}}return u}return function(a,c){if(Array.isArray(a))return a;if(Symbol.iterator in Object(a))return s(a,c);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})();Object.defineProperty(t,"__esModule",{value:!0});var n=ue(),o=fe();function i(s,a){var c=arguments.length<=2||arguments[2]===void 0?1e-6:arguments[2],u=-c,f=1+c,l=Math,h=l.min,d=l.max,b=["000","fff"].map(function(R){return a.fromXyz(s.fromRgb(n.rgb.fromHex(R)))}),g=r(b,2),m=g[0],y=g[1];function L(R){var M=s.toRgb(a.toXyz(R)),p=M.map(function(C){return C>=u&&C<=f}).reduce(function(C,O){return C&&O},!0);return[p,M]}function w(R,M){for(var p=arguments.length<=2||arguments[2]===void 0?.001:arguments[2];(0,o.distance)(R,M)>p;){var C=(0,o.lerp)(R,M,.5),O=L(C),q=r(O,1),T=q[0];T?R=C:M=C}return R}function x(R){return(0,o.lerp)(m,y,R)}function S(R){return R.map(function(M){return d(u,h(f,M))})}return{contains:L,limit:w,spine:x,crop:S}}t.default=i,e.exports=t.default})(he,he.exports)),he.exports}var de={exports:{}},it={},Os;function As(){if(Os)return it;Os=1;var e=(function(){function l(h,d){var b=[],g=!0,m=!1,y=void 0;try{for(var L=h[Symbol.iterator](),w;!(g=(w=L.next()).done)&&(b.push(w.value),!(d&&b.length===d));g=!0);}catch(x){m=!0,y=x}finally{try{!g&&L.return&&L.return()}finally{if(m)throw y}}return b}return function(h,d){if(Array.isArray(h))return h;if(Symbol.iterator in Object(h))return l(h,d);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})();Object.defineProperty(it,"__esModule",{value:!0}),it.toNotation=it.fromNotation=it.toHue=it.fromHue=void 0;var t=fe(),r=Math,n=r.floor,o=[{s:"R",h:20.14,e:.8,H:0},{s:"Y",h:90,e:.7,H:100},{s:"G",h:164.25,e:1,H:200},{s:"B",h:237.53,e:1.2,H:300},{s:"R",h:380.14,e:.8,H:400}],i=o.map(function(l){return l.s}).slice(0,-1).join("");function s(l){l50){var g=[d,h];h=g[0],d=g[1],b=100-b}return b<1?i[h]:i[h]+b.toFixed()+i[d]}return it.fromHue=s,it.toHue=a,it.fromNotation=u,it.toNotation=f,it}var Ss;function jc(){return Ss||(Ss=1,(function(e,t){var r=(function(){function P(G,Y){var U=[],ut=!0,xt=!1,Jt=void 0;try{for(var lt=G[Symbol.iterator](),Mt;!(ut=(Mt=lt.next()).done)&&(U.push(Mt.value),!(Y&&U.length===Y));ut=!0);}catch(Ct){xt=!0,Jt=Ct}finally{try{!ut&<.return&<.return()}finally{if(xt)throw Jt}}return U}return function(G,Y){if(Array.isArray(G))return G;if(Symbol.iterator in Object(G))return P(G,Y);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})();Object.defineProperty(t,"__esModule",{value:!0});var n=ue(),o=As(),i=c(o),s=fe(),a=Ur();function c(P){if(P&&P.__esModule)return P;var G={};if(P!=null)for(var Y in P)Object.prototype.hasOwnProperty.call(P,Y)&&(G[Y]=P[Y]);return G.default=P,G}var u=Math,f=u.pow,l=u.sqrt,h=u.exp,d=u.abs,b=u.sign,g=Math,m=g.sin,y=g.cos,L=g.atan2,w={average:{F:1,c:.69,N_c:1},dim:{F:.9,c:.59,N_c:.9},dark:{F:.8,c:.535,N_c:.8}},x=[[.7328,.4296,-.1624],[-.7036,1.6975,.0061],[.003,.0136,.9834]],S=[[.38971,.68898,-.07868],[-.22981,1.1834,.04641],[0,0,1]],R=x,M=n.matrix.inverse(x),p=n.matrix.product(S,n.matrix.inverse(x)),C=n.matrix.product(x,n.matrix.inverse(S)),O={whitePoint:n.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:"average",discounting:!1},q=(0,s.cfs)("QJMCshH"),T=(0,s.cfs)("JCh");function A(){var P=arguments.length<=0||arguments[0]===void 0?{}:arguments[0],G=arguments.length<=1||arguments[1]===void 0?q:arguments[1];P=(0,a.merge)(O,P);var Y=P.whitePoint,U=P.adaptingLuminance,ut=P.backgroundLuminance,xt=w[P.surroundType],Jt=xt.F,lt=xt.c,Mt=xt.N_c,Ct=Y[1],Ys=1/(5*U+1),Ot=.2*f(Ys,4)*5*U+.1*f(1-f(Ys,4),2)*f(5*U,1/3),_e=ut/Ct,an=.725*f(1/_e,.2),Zs=an,Js=1.48+l(_e),Hs=P.discounting?1:Jt*(1-1/3.6*h(-(U+42)/92)),d0=n.matrix.multiply(x,Y),b0=d0.map(function(j){return Hs*Ct/j+1-Hs}),cn=r(b0,3),Ws=cn[0],Us=cn[1],Qs=cn[2],p0=ti(Y),m0=ei(p0),ve=ri(m0);function ti(j){var z=n.matrix.multiply(R,j),B=r(z,3),Z=B[0],D=B[1],tt=B[2];return[Ws*Z,Us*D,Qs*tt]}function g0(j){var z=r(j,3),B=z[0],Z=z[1],D=z[2];return n.matrix.multiply(M,[B/Ws,Z/Us,D/Qs])}function ei(j){return n.matrix.multiply(p,j).map(function(z){var B=f(Ot*d(z)/100,.42);return b(z)*400*B/(27.13+B)+.1})}function _0(j){return n.matrix.multiply(C,j.map(function(z){var B=z-.1;return b(B)*100/Ot*f(27.13*d(B)/(400-d(B)),2.380952380952381)}))}function ri(j){var z=r(j,3),B=z[0],Z=z[1],D=z[2];return(B*2+Z+D/20-.305)*an}function un(j){return 4/lt*l(j/100)*(ve+4)*f(Ot,.25)}function v0(j){return 6.25*f(lt*j/((ve+4)*f(Ot,.25)),2)}function ni(j){return j*f(Ot,.25)}function y0(j,z){return f(j/100,2)*z/f(Ot,.25)}function w0(j){return j/f(Ot,.25)}function x0(j,z){return 100*l(j/z)}function ln(j,z){var B=z.Q,Z=z.J,D=z.M,tt=z.C,at=z.s,mt=z.h,gt=z.H,J={};return j.J&&(J.J=isNaN(Z)?v0(B):Z),j.C&&(isNaN(tt)?isNaN(D)?(B=isNaN(B)?un(Z):B,J.C=y0(at,B)):J.C=w0(D):J.C=z.C),j.h&&(J.h=isNaN(mt)?i.toHue(gt):mt),j.Q&&(J.Q=isNaN(B)?un(Z):B),j.M&&(J.M=isNaN(D)?ni(tt):D),j.s&&(isNaN(at)?(B=isNaN(B)?un(Z):B,D=isNaN(D)?ni(tt):D,J.s=x0(D,B)):J.s=at),j.H&&(J.H=isNaN(gt)?i.fromHue(mt):gt),J}function C0(j){var z=ti(j),B=ei(z),Z=r(B,3),D=Z[0],tt=Z[1],at=Z[2],mt=D-tt*12/11+at/11,gt=(D+tt-2*at)/9,J=L(gt,mt),Ft=n.degree.fromRadian(J),ye=1/4*(y(J+2)+3.8),we=ri(B),Ht=100*f(we/ve,lt*Js),kt=5e4/13*Mt*Zs*ye*l(mt*mt+gt*gt)/(D+tt+21/20*at),Rt=f(kt,.9)*l(Ht/100)*f(1.64-f(.29,_e),.73);return ln(G,{J:Ht,C:Rt,h:Ft})}function k0(j){var z=ln(T,j),B=z.J,Z=z.C,D=z.h,tt=n.degree.toRadian(D),at=f(Z/(l(B/100)*f(1.64-f(.29,_e),.73)),10/9),mt=1/4*(y(tt+2)+3.8),gt=ve*f(B/100,1/lt/Js),J=5e4/13*Mt*Zs*mt/at,Ft=gt/an+.305,ye=Ft*61/20*460/1403,we=61/20*220/1403,Ht=21/20*6300/1403-27/1403,kt=m(tt),Rt=y(tt),At,St;at===0||isNaN(at)?At=St=0:d(kt)>=d(Rt)?(St=ye/(J/kt+we*Rt/kt+Ht),At=St*Rt/kt):(At=ye/(J/Rt+we+Ht*kt/Rt),St=At*kt/Rt);var R0=[20/61*Ft+451/1403*At+288/1403*St,20/61*Ft-891/1403*At-261/1403*St,20/61*Ft-220/1403*At-6300/1403*St],q0=_0(R0),M0=g0(q0);return M0}return{fromXyz:C0,toXyz:k0,fillOut:ln}}t.default=A,e.exports=t.default})(de,de.exports)),de.exports}var be={exports:{}},$s;function Pc(){return $s||($s=1,(function(e,t){Object.defineProperty(t,"__esModule",{value:!0});var r=ue(),n=Math,o=n.sqrt,i=n.pow,s=n.exp,a=n.log,c=n.cos,u=n.sin,f=n.atan2,l={LCD:{K_L:.77,c_1:.007,c_2:.0053},SCD:{K_L:1.24,c_1:.007,c_2:.0363},UCS:{K_L:1,c_1:.007,c_2:.0228}};function h(){var d=arguments.length<=0||arguments[0]===void 0?"UCS":arguments[0],b=l[d],g=b.K_L,m=b.c_1,y=b.c_2;function L(S){var R=S.J,M=S.M,p=S.h,C=r.degree.toRadian(p),O=(1+100*m)*R/(1+m*R),q=1/y*a(1+y*M),T=q*c(C),A=q*u(C);return{J_p:O,a_p:T,b_p:A}}function w(S){var R=S.J_p,M=S.a_p,p=S.b_p,C=-R/(m*R-100*m-1),O=o(i(M,2)+i(p,2)),q=(s(y*O)-1)/y,T=f(p,M),A=r.degree.fromRadian(T);return{J:C,M:q,h:A}}function x(S,R){return o(i((S.J_p-R.J_p)/g,2)+i(S.a_p-R.a_p,2)+i(S.b_p-R.b_p,2))}return{fromCam:L,toCam:w,distance:x}}t.default=h,e.exports=t.default})(be,be.exports)),be.exports}var tn,Es;function zc(){if(Es)return tn;Es=1;var e=fe(),t=Tc(),r=jc(),n=Pc(),o=As();return tn={gamut:t,cfs:e.cfs,lerp:e.lerp,cam:r,ucs:n,hq:o},tn}var Bc=zc();const Ls=to(Bc);function Ns(e){const t=new v;return t.rgb_r=e[0],t.rgb_g=e[1],t.rgb_b=e[2],t.rgbToHsluv(),[t.hsluv_h,t.hsluv_s,t.hsluv_l]}function Ic(e){const t=new v;return t.hsluv_h=e[0],t.hsluv_s=e[1],t.hsluv_l=e[2],t.hsluvToRgb(),[t.rgb_r,t.rgb_g,t.rgb_b]}const Ts=Ls.cam({whitePoint:le.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:"average",discounting:!1},Ls.cfs("JCh")),js=le.xyz(le.workspace.sRGB,le.illuminant.D65),Ps=e=>js.toRgb(Ts.toXyz({J:e[0],C:e[1],h:e[2]})),en=e=>{const t=Ts.fromXyz(js.fromRgb(e));return[t.J,t.C,t.h]},[Gc,Fc]=(()=>{const e={k_l:1,c1:.007,c2:.0228},t=Math.PI,r=64/t/5,n=1/(5*r+1),o=.2*n**4*(5*r)+.1*(1-n**4)**2*(5*r)**(1/3);return[i=>{const[s,a,c]=i,u=a*o**.25;let f=(1+100*e.c1)*s/(1+e.c1*s);f/=e.k_l;const l=1/e.c2*Math.log(1+e.c2*u),h=l*Math.cos(c*(t/180)),d=l*Math.sin(c*(t/180));return[f,h,d]},i=>{const[s,a,c]=i,u=Math.sqrt(a*a+c*c),f=(Math.exp(u*e.c2)-1)/e.c2,l=(180/t*Math.atan2(c,a)+360)%360,h=f/o**.25;return[s/(1+e.c1*(100-s)),h,l]}]})(),Kc=e=>Ps(Fc(e)),zs=e=>Gc(en(e)),pe=console;pe.color=(e,t="")=>{const n=k(e).luminance();pe.log(`%c${e} ${t}`,`background-color: ${e};padding: 5px; border-radius: 5px; color: ${n>.5?"#000":"#fff"}`)},pe.ramp=(e,t=1)=>{pe.log("%c ",`font-size: 1px;line-height: 16px;background: ${k.getCSSGradient(e,t)};padding: 0 0 0 200px; border-radius: 2px;`)};const Bs=(e,t,r,n,o,i,s=.1)=>{if(e===r||t===n)return!0;const a=(n-t)/(r-e),c=(i+o/a-t+a*e)/(a+1/a),u=i+o/a-c/a;return(o-c)**2+(i-u)**2{const o=(t[0]+r[0])/2,i=e(o);return Bs(...t,...r,o,i,n)?null:[o,i]},rn=(e,t,r,n=.1)=>{const o=(r-t)/10,i=[];for(let s=t;sMath.round(e*10**t)/10**t,Dc=(e,t=1,r=90,n=.005)=>{const o=rn(c=>e(c).gl()[0],0,t,n),i=rn(c=>e(c).gl()[1],0,t,n),s=rn(c=>e(c).gl()[2],0,t,n),a=Array.from(new Set([...o.map(c=>me(c[0])),...i.map(c=>me(c[0])),...s.map(c=>me(c[0]))].sort((c,u)=>c-u)));return`linear-gradient(${r}deg, ${a.map(c=>`${e(c).hex()} ${me(c*100)}%`).join()});`},Vc=e=>{e.Color.prototype.jch=function(){return en(this._rgb.slice(0,3).map(o=>o/255))},e.jch=(...o)=>new e.Color(...Ps(o).map(i=>Math.floor(i*255)),"rgb"),e.Color.prototype.jab=function(){return zs(this._rgb.slice(0,3).map(o=>o/255))},e.jab=(...o)=>new e.Color(...Kc(o).map(i=>Math.floor(i*255)),"rgb"),e.Color.prototype.hsluv=function(){return Ns(this._rgb.slice(0,3).map(o=>o/255))},e.hsluv=(...o)=>new e.Color(...Ic(o).map(i=>Math.floor(i*255)),"rgb");const t=e.interpolate,r={jch:en,jab:zs,hsluv:Ns},n=(o,i,s)=>(Math.abs(o-i)>360/2&&(o>i?i+=360:o+=360),((1-s)*o+s*i)%360);e.interpolate=(o,i,s=.5,a="lrgb")=>{if(r[a]){typeof o!="object"&&(o=new e.Color(o)),typeof i!="object"&&(i=new e.Color(i));const c=r[a](o.gl()),u=r[a](i.gl()),f=Number.isNaN(o.hsl()[0]),l=Number.isNaN(i.hsl()[0]);let h,d,b;switch(a){case"hsluv":c[1]<1e-10&&(c[0]=u[0]),c[1]===0&&(c[1]=u[1]),u[1]<1e-10&&(u[0]=c[0]),u[1]===0&&(u[1]=c[1]),h=n(c[0],u[0],s),d=c[1]+(u[1]-c[1])*s,b=c[2]+(u[2]-c[2])*s;break;case"jch":f&&(c[2]=u[2]),l&&(u[2]=c[2]),h=c[0]+(u[0]-c[0])*s,d=c[1]+(u[1]-c[1])*s,b=n(c[2],u[2],s);break;default:h=c[0]+(u[0]-c[0])*s,d=c[1]+(u[1]-c[1])*s,b=c[2]+(u[2]-c[2])*s}return e[a](h,d,b).alpha(o.alpha()+s*(i.alpha()-o.alpha()))}return t(o,i,s,a)},e.getCSSGradient=Dc};const X={mainTRC:2.4,sRco:.2126729,sGco:.7151522,sBco:.072175,normBG:.56,normTXT:.57,revTXT:.62,revBG:.65,blkThrs:.022,blkClmp:1.414,scaleBoW:1.14,scaleWoB:1.14,loBoWoffset:.027,loWoBoffset:.027,deltaYmin:5e-4,loClip:.1};function Is(e,t,r=-1){const n=[0,1.1];if(isNaN(e)||isNaN(t)||Math.min(e,t)n[1])return 0;let o=0,i=0,s="BoW";return e=e>X.blkThrs?e:e+Math.pow(X.blkThrs-e,X.blkClmp),t=t>X.blkThrs?t:t+Math.pow(X.blkThrs-t,X.blkClmp),Math.abs(t-e)e?(o=(Math.pow(t,X.normBG)-Math.pow(e,X.normTXT))*X.scaleBoW,i=o-.1?0:o+X.loWoBoffset),r<0?i*100:r==0?Math.round(Math.abs(i)*100)+""+s+"":Number.isInteger(r)?(i*100).toFixed(r):0)}function ge(e=[0,0,0]){function t(r){return Math.pow(r/255,X.mainTRC)}return X.sRco*t(e[0])+X.sGco*t(e[1])+X.sBco*t(e[2])}const Gs=(e,t,r,n,o,i,s,a,c)=>{const u=1-c,f=u*u,l=f*u,d=c*c*c,b=l*e+f*3*c*r+u*3*c*c*o+d*s,g=l*t+f*3*c*n+u*3*c*c*i+d*a;return{x:b,y:g}},Yc=(e,t)=>{const r=[];let n={x:+e[0],y:+e[1]};for(let o=0,i=e.length;i-2*!0>o;o+=2){const s=[{x:+e[o-2],y:+e[o-1]},{x:+e[o],y:+e[o+1]},{x:+e[o+2],y:+e[o+3]},{x:+e[o+4],y:+e[o+5]}];i-4===o?s[3]=s[2]:o||(s[0]={x:+e[o],y:+e[o+1]}),r.push([n.x,n.y,(-s[0].x+6*s[1].x+s[2].x)/6,(-s[0].y+6*s[1].y+s[2].y)/6,(s[1].x+6*s[2].x-s[3].x)/6,(s[1].y+6*s[2].y-s[3].y)/6,s[2].x,s[2].y]),n=s[2]}return r},Zc=(e,t,r,n,o,i,s,a)=>{let u=e,f=t,l=0;for(let h=1;h<5;h++){const{x:d,y:b}=Gs(e,t,r,n,o,i,s,a,h/5);l+=Math.hypot(d-u,b-f),u=d,f=b}return l+=Math.hypot(s-u,a-f),l},Jc=(e,t,r,n,o,i,s,a)=>{const c=Math.floor(Zc(e,t,r,n,o,i,s,a)*.75),u=[];let f=0;for(let l=0;l<=c;l++){const h=l/c,d=Gs(e,t,r,n,o,i,s,a,h),b=Math.round(d.x);if(u[b]=d.y,b-f>1){const g=u[f],m=u[b];for(let y=f+1;yu[Math.round(l)]||null},Gt={CAM02:"jab",CAM02p:"jch",HEX:"hex",HSL:"hsl",HSLuv:"hsluv",HSV:"hsv",LAB:"lab",LCH:"lch",RGB:"rgb",OKLAB:"oklab",OKLCH:"oklch"};function wt(e,t=0){const r=10**t;return Math.round(e*r)/r}function Hc(e,t){let r;return e>1?r=(e-1)*t+1:e<-1?r=(e+1)*t-1:r=1,wt(r,2)}function Wc(e){return k(String(e)).jch()}function Uc(e){return k(String(e)).hsluv()}function Qc(e,t,r){const n=[[],[],[]];if(e.forEach((i,s)=>n.forEach((a,c)=>a.push(t[s],i[c]))),r==="hcl"){const i=n[1];for(let s=1;s{const s=[];for(let a=1;a{i[c]=i[a]}),s.length=0;break}if(s.length){const a=k("#ccc").jch()[2];s.forEach(c=>{i[c]=a})}s.length=0;for(let a=i.length-1;a>0;a-=2)if(Number.isNaN(i[a]))s.push(a);else{s.forEach(c=>{i[c]=i[a]});break}for(let a=1;aYc(i).map(s=>Jc(...s)));return i=>{const s=o.map(a=>{for(let c=0;cn*i**e+o}function nn({swatches:e,colorKeys:t,colorspace:r,colorSpace:n=r??"LAB",shift:o=1,fullScale:i=!0,smooth:s=!1,distributeLightness:a="linear",sortColor:c=!0,asFun:u=!1}={}){r!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead.");const f=Gt[n];if(!f)throw new Error(`Colorspace “${n}” not supported`);if(!t)throw new Error(`Colorkeys missing: returned “${t}”`);let l;if(i)l=t.map(w=>e-e*(k(w).jch()[0]/100)).sort((w,x)=>w-x).concat(e),l.unshift(0);else{let w=t.map(R=>k(R).jch()[0]/100),x=Math.min(...w),S=Math.max(...w);l=w.map(R=>R===0||isNaN((R-x)/(S-x))?0:e-(R-x)/(S-x)*e).sort((R,M)=>R-M)}let h=t0(o,[1,e],[1,e]);if(h=l.map(w=>Math.max(0,h(w))),l=h,a==="polynomial"){const w=R=>Math.sqrt(Math.sqrt((Math.pow(R,2.25)+Math.pow(R,4))/2));l=h.map(R=>R/e).map(R=>w(R)*e)}const d=t.map((w,x)=>({colorKeys:Wc(w),index:x})).sort((w,x)=>x.colorKeys[0]-w.colorKeys[0]).map(w=>t[w.index]);let b=[],g;if(i){const w=f==="lch"?k.lch(...k("#fff").lch()):"#ffffff",x=f==="lch"?k.lch(...k("#000").lch()):"#000000";b=[w,...d,x]}else c?b=d:b=t;let m;if(s){const w=b;if(b=b.map(x=>k(String(x))[f]()),f==="hcl"&&b.forEach(x=>{x[1]=Number.isNaN(x[1])?0:x[1]}),f==="jch")for(let x=0;xg(S))}else g=k.scale(b.map(w=>typeof w=="object"&&w.constructor===k.Color?w:String(w))).domain(l).mode(f);return u?g:(!s||s===!1?g.colors(e):m).filter(w=>w!=null)}function e0(e,t){const r=[],n={};return Object.keys(e).forEach(s=>{n[e[s][t]]=e[s]}),Object.keys(n).forEach(s=>r.push(n[s])),r}function r0(e){return Number.isNaN(e)?0:e}function on(e,t,r=!1){if(!e)throw new Error(`Cannot convert color value of “${e}”`);if(!Gt[t])throw new Error(`Cannot convert to colorspace “${t}”`);const n=Gt[t],o=k(String(e))[n]();if(t==="HSL"&&o.pop(),t==="HEX"){if(r){const u=k(String(e)).rgb();return{r:u[0],g:u[1],b:u[2]}}return o}const i={};let s=o.map(r0);s=s.map((u,f)=>{let l=wt(u),h=f;n==="hsluv"&&(h+=2);let d=n.charAt(h);return n==="jch"&&d==="c"&&(d="C"),i[d==="j"?"J":d]=l,n in{lab:1,lch:1,jab:1,jch:1}?r||(d==="l"||d==="j")&&(l+="%"):n!=="hsluv"&&(d==="s"||d==="l"||d==="v")&&(i[d]=wt(u,2),r||(l=wt(u*100),l+="%")),l});const c=`${n}(${s.join(", ")})`;return r?i:c}function Fs(e,t,r){const n=[e,t,r].map(o=>(o/=255,o<=.03928?o/12.92:((o+.055)/1.055)**2.4));return n[0]*.2126+n[1]*.7152+n[2]*.0722}function n0(e,t,r,n="wcag2"){if(r===void 0){const o=k.rgb(...t).hsluv()[2];r=wt(o/100,2)}if(n==="wcag2"){const o=Fs(e[0],e[1],e[2]),i=Fs(t[0],t[1],t[2]),s=(o+.05)/(i+.05),a=(i+.05)/(o+.05);return r<.5?s>=1?s:-a:s<1?a:s===1?s:-s}else{if(n==="wcag3")return r<.5?Is(ge(e),ge(t))*-1:Is(ge(e),ge(t));throw new Error(`Contrast calculation method ${n} unsupported; use 'wcag2' or 'wcag3'`)}}function o0(e,t){if(!e)throw new Error("Array undefined");if(!Array.isArray(e))throw new Error("Passed object is not an array");const r=t==="wcag2"?0:1;return Math.min(...e.filter(n=>n>=r))}function s0(e,t){if(!e)throw new Error("Ratios undefined");e=e.sort((a,c)=>a-c);const r=o0(e,t),n=e.indexOf(r),o=[],i=e.slice(0,n),s=e.slice(n,e.length);for(let a=0;aa-c),o}const i0=(e,t,r,n,o)=>{const s=nn({swatches:3e3,colorKeys:e._modifiedKeys,colorspace:e._colorspace,shift:1,smooth:e._smooth,asFun:!0}),a={},c=l=>{if(a[l])return a[l];const h=k(s(l)).rgb(),d=n0(h,t,r,o);return a[l]=d,d},u=l=>{const h=c(0),d=c(3e3),b=hg&&w;)w--,m/=2,Lf.push(s(u(+l)))),f};class Q{constructor({name:t,colorKeys:r,colorspace:n,colorSpace:o=n??"RGB",ratios:i,smooth:s=!1,output:a="HEX",saturation:c=100}){if(n!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),this._name=t,this._colorKeys=r,this._modifiedKeys=r,this._colorspace=o,this._ratios=i,this._smooth=s,this._output=a,this._saturation=c,!this._name)throw new Error("Color missing name");if(!this._colorKeys)throw new Error("Color Keys are undefined");if(!Gt[this._colorspace])throw new Error(`Colorspace “${o}” not supported`);if(!Gt[this._output])throw new Error(`Output “${this._output}” not supported`);for(let u=0;u{let n=k(`${r}`).oklch(),i=n[1]*(this._saturation/100),s=k.oklch(n[0],i,n[2]),a=k.rgb(s).hex();t.push(a)}),this._modifiedKeys=t,this._generateColorScale()}_generateColorScale(){this._colorScale=nn({swatches:3e3,colorKeys:this._modifiedKeys,colorSpace:this._colorspace,shift:1,smooth:this._smooth,asFun:!0})}}class Ks extends Q{get backgroundColorScale(){return this._backgroundColorScale||this._generateColorScale(),this._backgroundColorScale}_generateColorScale(){Q.prototype._generateColorScale.call(this);const t=nn({swatches:1e3,colorKeys:this._colorKeys,colorspace:this._colorspace,shift:1,smooth:this._smooth});t.push(...this.colorKeys);const r=t.map((i,s)=>({value:Math.round(Uc(i)[2]),index:s})),o=e0(r,"value").map(i=>t[i.index]);return o.length>=101&&(o.length=100,o.push("#ffffff")),this._backgroundColorScale=o.map(i=>on(i,this._output)),this._backgroundColorScale}}class a0{constructor({colors:t,backgroundColor:r,lightness:n,contrast:o=1,saturation:i=100,output:s="HEX",formula:a="wcag2"}){if(this._output=s,this._colors=t,this._lightness=n,this._saturation=i,this._formula=a,this._setBackgroundColor(r),this._setBackgroundColorValue(),this._contrast=o,!this._colors)throw new Error("No colors are defined");if(!this._backgroundColor)throw new Error("Background color is undefined");if(t.forEach(c=>{if(!c.ratios)throw new Error(`Color ${c.name}'s ratios are undefined`)}),!Gt[this._output])throw new Error(`Output “${s}” not supported`);this._saturation<100&&this._updateColorSaturation(this._saturation),this._findContrastColors(),this._findContrastColorPairs(),this._findContrastColorValues()}set formula(t){this._formula=t,this._findContrastColors()}get formula(){return this._formula}set contrast(t){this._contrast=t,this._findContrastColors()}get contrast(){return this._contrast}set lightness(t){this._lightness=t,this._setBackgroundColor(this._backgroundColor),this._findContrastColors()}get lightness(){return this._lightness}set saturation(t){this._saturation=t,this._updateColorSaturation(t),this._findContrastColors()}get saturation(){return this._saturation}set backgroundColor(t){this._setBackgroundColor(t),this._findContrastColors()}get backgroundColorValue(){return this._backgroundColorValue}get backgroundColor(){return this._backgroundColor}set colors(t){this._colors=t,this._findContrastColors()}get colors(){return this._colors}set addColor(t){this._colors.push(t),this._findContrastColors()}set removeColor(t){const r=this._colors.filter(n=>n.name!==t.name);this._colors=r,this._findContrastColors()}set updateColor(t){if(Array.isArray(t))for(let r=0;rs.name===t[r].color);n=n[0];let o=this._colors.indexOf(n);const i=this._colors.filter(s=>s.name!==t[r].color);t[r].name&&(n.name=t[r].name),t[r].colorKeys&&(n.colorKeys=t[r].colorKeys),t[r].ratios&&(n.ratios=t[r].ratios),(t[r].colorSpace!==void 0||t[r].colorspace!==void 0)&&(t[r].colorspace!==void 0&&t[r].colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),n.colorSpace=t[r].colorSpace??t[r].colorspace),t[r].smooth&&(n.smooth=t[r].smooth),n._generateColorScale(),i.splice(o,0,n),this._colors=i}else{let r=this._colors.filter(i=>i.name===t.color);r=r[0];let n=this._colors.indexOf(r);const o=this._colors.filter(i=>i.name!==t.color);t.name&&(r.name=t.name),t.colorKeys&&(r.colorKeys=t.colorKeys),t.ratios&&(r.ratios=t.ratios),(t.colorSpace!==void 0||t.colorspace!==void 0)&&(t.colorspace!==void 0&&t.colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),r.colorSpace=t.colorSpace??t.colorspace),t.smooth&&(r.smooth=t.smooth),r._generateColorScale(),o.splice(n,0,r),this._colors=o}this._findContrastColors()}set output(t){this._output=t,this._colors.forEach(r=>{r.output=this._output}),this._backgroundColor.output=this._output,this._findContrastColors()}get output(){return this._output}get contrastColors(){return this._contrastColors}get contrastColorPairs(){return this._contrastColorPairs}get contrastColorValues(){return this._contrastColorValues}_setBackgroundColor(t){if(typeof t=="string"){const r=new Ks({name:"background",colorKeys:[t],output:"RGB"}),n=wt(k(String(t)).hsluv()[2]);this._backgroundColor=r,this._lightness=n,this._backgroundColorValue=r[this._lightness]}else{t.output="RGB";const r=t.backgroundColorScale[this._lightness];this._backgroundColor=t,this._backgroundColorValue=r}}_setBackgroundColorValue(){this._backgroundColorValue=this._backgroundColor.backgroundColorScale[this._lightness]}_updateColorSaturation(t){this._colors.map(r=>{r.saturation=t})}_findContrastColors(){const t=k(String(this._backgroundColorValue)).rgb(),r=this._lightness/100,o={background:on(this._backgroundColorValue,this._output)},i=[],s=[],a={...o};return i.push(o),this._colors.map(c=>{if(c.ratios!==void 0){let u;const f=[],l={name:c.name,values:f};let h;Array.isArray(c.ratios)?h=c.ratios:Array.isArray(c.ratios)||(u=Object.keys(c.ratios),h=Object.values(c.ratios)),h=h.map(b=>Hc(+b,this._contrast));const d=i0(c,t,r,h,this._formula).map(b=>on(b,this._output));for(let b=0;b{const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return[Number.parseInt(t[1],16),Number.parseInt(t[2],16),Number.parseInt(t[3],16)]},Ds=(e,t,r)=>{const n=e/255,o=t/255,i=r/255,s=Math.min(n,o,i),a=Math.max(n,o,i),c=a-s;let u=0,f=0,l=0;return c===0?u=0:a===n?u=(o-i)/c%6:a===o?u=(i-n)/c+2:u=(n-o)/c+4,u=Math.round(u*60),u<0&&(u+=360),l=(a+s)/2,f=c===0?0:c/(1-Math.abs(2*l-1)),f=+(f*100).toFixed(1),l=+(l*100).toFixed(1),[u,f,Math.round(l)]},u0=(e,t,r,n)=>{const o=r/100,i=t*Math.min(o,1-o)/100,s=d=>{const b=(d+e/30)%12,g=o-i*Math.max(Math.min(b-3,9-b,1),-1);return Math.round(255*g).toString(16).padStart(2,"0").toUpperCase()},a=s(0),c=s(8),u=s(4),l=((d,b,g)=>Math.min(Math.max(d,b),g))(n,0,1),h=Math.round(l*255).toString(16).padStart(2,"0").toUpperCase();return`#${a}${c}${u}${h}`},l0=(e,t,r=1)=>{const n=Xs(e),o=Xs(t==="white"?"#FFFFFF":t==="black"?"#000000":t),i=n.map((u,f)=>[(u-o[f])/(255-o[f]),(u-o[f])/(0-o[f])]),s=c0(Math.max(...i.flat().filter(u=>/^-?\d+\.?\d*$/.test(u)))),a=n.map((u,f)=>Math.round((u-o[f]+o[f]*s)/s));if(a.includes(Number.NaN)){const u=Ds(n[0],n[1],n[2]);return{h:u[0],s:Math.round(u[1]*r),l:u[2],a:1}}const c=Ds(a[0],a[1],a[2]);return{h:c[0],s:Math.round(c[1]*r),l:c[2],a:s}},sn={backgroundColor:"gray",colorSpace:"OKLCH",colorSmoothing:!1,formula:"wcag2",output:"HEX",colors:{gray:[I(215,20,90),I(215,8,50),I(215,6,25)],red:[I(358,100,58),I(350,100,30)],orange:[I(32,100,48),I(12,100,30)],yellow:[I(50,100,50),I(25,100,20)],lime:[I(100,68,50),I(115,86,25)],green:[I(163,87,42),I(168,100,25)],cyan:[I(185,80,45),I(200,98,35)],blue:[I(212,98,46),I(222,95,25)],purple:[I(258,94,64),I(265,100,35)],fuchsia:[I(295,56,50),I(285,80,25)],pink:[I(334,90,50),I(330,91,25)]},themes:{light:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16.75],contrast:1,lightness:100,saturation:100},dark:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16],contrast:1,lightness:6,saturation:97},lightHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:100,saturation:100},darkHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:6,saturation:97}}};function I(e,t,r){return k.hsl(e,t/100,r/100).hex()}function f0(e,t){const r=e.colorSpace,n=e.colorSmoothing,o=e.themes[t].ratios,i=new Ks({name:"gray",colorKeys:e.colors.gray,colorspace:r,ratios:o,smooth:n}),s=new Q({name:"blue",colorKeys:e.colors.blue,colorspace:r,ratios:o,smooth:n}),a=new Q({name:"cyan",colorKeys:e.colors.cyan,colorspace:r,ratios:o,smooth:n}),c=new Q({name:"fuchsia",colorKeys:e.colors.fuchsia,colorspace:r,ratios:o,smooth:n}),u=new Q({name:"green",colorKeys:e.colors.green,colorspace:r,ratios:o,smooth:n}),f=new Q({name:"lime",colorKeys:e.colors.lime,colorspace:r,ratios:o,smooth:n}),l=new Q({name:"orange",colorKeys:e.colors.orange,colorspace:r,ratios:o,smooth:n}),h=new Q({name:"pink",colorKeys:e.colors.pink,colorspace:r,ratios:o,smooth:n}),d=new Q({name:"purple",colorKeys:e.colors.purple,colorspace:r,ratios:o,smooth:n}),b=new Q({name:"red",colorKeys:e.colors.red,colorspace:r,ratios:o,smooth:n}),g=new Q({name:"yellow",colorKeys:e.colors.yellow,colorspace:r,ratios:o,smooth:n}),m={gray:i,red:b,orange:l,yellow:g,lime:f,green:u,cyan:a,blue:s,purple:d,fuchsia:c,pink:h};return e.colors.custom&&(m.custom=new Q({name:"custom",colorKeys:e.colors.custom,colorspace:r,ratios:o,smooth:n})),new a0({colors:Object.values(m),backgroundColor:m[e.backgroundColor],contrast:e.themes[t].contrast,lightness:e.themes[t].lightness,saturation:e.themes[t].saturation,output:e.output,formula:e.formula}).contrastColors}function Vs(e){const t={};for(const r of Object.keys(e.themes))t[r]=f0(e,r);return t}function h0(e){sn.colors.custom=[e];const t=Vs(sn);return Object.fromEntries(Object.entries(t).map(([r,n])=>{const o=n.find(s=>s&&s.name==="custom"),i=Object.fromEntries(o.values.map(({name:s,value:a})=>[s,a]));for(const[s,a]of Object.entries(i)){const c=l0(a,n[0].background);i[`alpha${s.charAt(0).toUpperCase()+s.slice(1)}`]=u0(c.h,c.s,c.l,c.a)}return[r,i]}))}return $t.generateCustomColors=h0,$t.generateThemesJson=Vs,$t.hslToHex=I,$t.leonardoConfig=sn,Object.defineProperty($t,Symbol.toStringTag,{value:"Module"}),$t})({}); +var CompoundTheme=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;lt[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule?n(i,`default`,{value:e,enumerable:!0}):i,e)),{min:u,max:d}=Math,f=(e,t=0,n=1)=>u(d(t,e),n),p=e=>{e._clipped=!1,e._unclipped=e.slice(0);for(let t=0;t<=3;t++)t<3?((e[t]<0||e[t]>255)&&(e._clipped=!0),e[t]=f(e[t],0,255)):t===3&&(e[t]=f(e[t],0,1));return e},m={};for(let e of[`Boolean`,`Number`,`String`,`Function`,`Array`,`Date`,`RegExp`,`Undefined`,`Null`])m[`[object ${e}]`]=e.toLowerCase();function h(e){return m[Object.prototype.toString.call(e)]||`object`}var g=(e,t=null)=>e.length>=3?Array.prototype.slice.call(e):h(e[0])==`object`&&t?t.split(``).filter(t=>e[0][t]!==void 0).map(t=>e[0][t]):e[0].slice(0),_=e=>{if(e.length<2)return null;let t=e.length-1;return h(e[t])==`string`?e[t].toLowerCase():null},{PI:v,min:y,max:b}=Math,x=e=>Math.round(e*100)/100,S=e=>Math.round(e*100)/100,C=v*2,w=v/3,T=v/180,ee=180/v;function E(e){return[...e.slice(0,3).reverse(),...e.slice(3)]}var D={format:{},autodetect:[]},O=class{constructor(...e){let t=this;if(h(e[0])===`object`&&e[0].constructor&&e[0].constructor===this.constructor)return e[0];let n=_(e),r=!1;if(!n){r=!0,D.sorted||=(D.autodetect=D.autodetect.sort((e,t)=>t.p-e.p),!0);for(let t of D.autodetect)if(n=t.test(...e),n)break}if(D.format[n])t._rgb=p(D.format[n].apply(null,r?e:e.slice(0,-1)));else throw Error(`unknown format: `+e);t._rgb.length===3&&t._rgb.push(1)}toString(){return h(this.hex)==`function`?this.hex():`[${this._rgb.join(`,`)}]`}},te=`3.2.0`,k=(...e)=>new O(...e);k.version=te;var A={aliceblue:`#f0f8ff`,antiquewhite:`#faebd7`,aqua:`#00ffff`,aquamarine:`#7fffd4`,azure:`#f0ffff`,beige:`#f5f5dc`,bisque:`#ffe4c4`,black:`#000000`,blanchedalmond:`#ffebcd`,blue:`#0000ff`,blueviolet:`#8a2be2`,brown:`#a52a2a`,burlywood:`#deb887`,cadetblue:`#5f9ea0`,chartreuse:`#7fff00`,chocolate:`#d2691e`,coral:`#ff7f50`,cornflowerblue:`#6495ed`,cornsilk:`#fff8dc`,crimson:`#dc143c`,cyan:`#00ffff`,darkblue:`#00008b`,darkcyan:`#008b8b`,darkgoldenrod:`#b8860b`,darkgray:`#a9a9a9`,darkgreen:`#006400`,darkgrey:`#a9a9a9`,darkkhaki:`#bdb76b`,darkmagenta:`#8b008b`,darkolivegreen:`#556b2f`,darkorange:`#ff8c00`,darkorchid:`#9932cc`,darkred:`#8b0000`,darksalmon:`#e9967a`,darkseagreen:`#8fbc8f`,darkslateblue:`#483d8b`,darkslategray:`#2f4f4f`,darkslategrey:`#2f4f4f`,darkturquoise:`#00ced1`,darkviolet:`#9400d3`,deeppink:`#ff1493`,deepskyblue:`#00bfff`,dimgray:`#696969`,dimgrey:`#696969`,dodgerblue:`#1e90ff`,firebrick:`#b22222`,floralwhite:`#fffaf0`,forestgreen:`#228b22`,fuchsia:`#ff00ff`,gainsboro:`#dcdcdc`,ghostwhite:`#f8f8ff`,gold:`#ffd700`,goldenrod:`#daa520`,gray:`#808080`,green:`#008000`,greenyellow:`#adff2f`,grey:`#808080`,honeydew:`#f0fff0`,hotpink:`#ff69b4`,indianred:`#cd5c5c`,indigo:`#4b0082`,ivory:`#fffff0`,khaki:`#f0e68c`,laserlemon:`#ffff54`,lavender:`#e6e6fa`,lavenderblush:`#fff0f5`,lawngreen:`#7cfc00`,lemonchiffon:`#fffacd`,lightblue:`#add8e6`,lightcoral:`#f08080`,lightcyan:`#e0ffff`,lightgoldenrod:`#fafad2`,lightgoldenrodyellow:`#fafad2`,lightgray:`#d3d3d3`,lightgreen:`#90ee90`,lightgrey:`#d3d3d3`,lightpink:`#ffb6c1`,lightsalmon:`#ffa07a`,lightseagreen:`#20b2aa`,lightskyblue:`#87cefa`,lightslategray:`#778899`,lightslategrey:`#778899`,lightsteelblue:`#b0c4de`,lightyellow:`#ffffe0`,lime:`#00ff00`,limegreen:`#32cd32`,linen:`#faf0e6`,magenta:`#ff00ff`,maroon:`#800000`,maroon2:`#7f0000`,maroon3:`#b03060`,mediumaquamarine:`#66cdaa`,mediumblue:`#0000cd`,mediumorchid:`#ba55d3`,mediumpurple:`#9370db`,mediumseagreen:`#3cb371`,mediumslateblue:`#7b68ee`,mediumspringgreen:`#00fa9a`,mediumturquoise:`#48d1cc`,mediumvioletred:`#c71585`,midnightblue:`#191970`,mintcream:`#f5fffa`,mistyrose:`#ffe4e1`,moccasin:`#ffe4b5`,navajowhite:`#ffdead`,navy:`#000080`,oldlace:`#fdf5e6`,olive:`#808000`,olivedrab:`#6b8e23`,orange:`#ffa500`,orangered:`#ff4500`,orchid:`#da70d6`,palegoldenrod:`#eee8aa`,palegreen:`#98fb98`,paleturquoise:`#afeeee`,palevioletred:`#db7093`,papayawhip:`#ffefd5`,peachpuff:`#ffdab9`,peru:`#cd853f`,pink:`#ffc0cb`,plum:`#dda0dd`,powderblue:`#b0e0e6`,purple:`#800080`,purple2:`#7f007f`,purple3:`#a020f0`,rebeccapurple:`#663399`,red:`#ff0000`,rosybrown:`#bc8f8f`,royalblue:`#4169e1`,saddlebrown:`#8b4513`,salmon:`#fa8072`,sandybrown:`#f4a460`,seagreen:`#2e8b57`,seashell:`#fff5ee`,sienna:`#a0522d`,silver:`#c0c0c0`,skyblue:`#87ceeb`,slateblue:`#6a5acd`,slategray:`#708090`,slategrey:`#708090`,snow:`#fffafa`,springgreen:`#00ff7f`,steelblue:`#4682b4`,tan:`#d2b48c`,teal:`#008080`,thistle:`#d8bfd8`,tomato:`#ff6347`,turquoise:`#40e0d0`,violet:`#ee82ee`,wheat:`#f5deb3`,white:`#ffffff`,whitesmoke:`#f5f5f5`,yellow:`#ffff00`,yellowgreen:`#9acd32`},ne=/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,re=/^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$/,ie=e=>{if(e.match(ne)){(e.length===4||e.length===7)&&(e=e.substr(1)),e.length===3&&(e=e.split(``),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]);let t=parseInt(e,16);return[t>>16,t>>8&255,t&255,1]}if(e.match(re)){(e.length===5||e.length===9)&&(e=e.substr(1)),e.length===4&&(e=e.split(``),e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]+e[3]+e[3]);let t=parseInt(e,16);return[t>>24&255,t>>16&255,t>>8&255,Math.round((t&255)/255*100)/100]}throw Error(`unknown hex color: ${e}`)},{round:ae}=Math,oe=(...e)=>{let[t,n,r,i]=g(e,`rgba`),a=_(e)||`auto`;i===void 0&&(i=1),a===`auto`&&(a=i<1?`rgba`:`rgb`),t=ae(t),n=ae(n),r=ae(r);let o=`000000`+(t<<16|n<<8|r).toString(16);o=o.substr(o.length-6);let s=`0`+ae(i*255).toString(16);switch(s=s.substr(s.length-2),a.toLowerCase()){case`rgba`:return`#${o}${s}`;case`argb`:return`#${s}${o}`;default:return`#${o}`}};O.prototype.name=function(){let e=oe(this._rgb,`rgb`);for(let t of Object.keys(A))if(A[t]===e)return t.toLowerCase();return e},D.format.named=e=>{if(e=e.toLowerCase(),A[e])return ie(A[e]);throw Error(`unknown color name: `+e)},D.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&h(e)===`string`&&A[e.toLowerCase()])return`named`}}),O.prototype.alpha=function(e,t=!1){return e!==void 0&&h(e)===`number`?t?(this._rgb[3]=e,this):new O([this._rgb[0],this._rgb[1],this._rgb[2],e],`rgb`):this._rgb[3]},O.prototype.clipped=function(){return this._rgb._clipped||!1};var j={Kn:18,labWhitePoint:`d65`,Xn:.95047,Yn:1,Zn:1.08883,t0:.137931034,t1:.206896552,t2:.12841855,t3:.008856452,kE:216/24389,kKE:8,kK:24389/27,RefWhiteRGB:{X:.95047,Y:1,Z:1.08883},MtxRGB2XYZ:{m00:.4124564390896922,m01:.21267285140562253,m02:.0193338955823293,m10:.357576077643909,m11:.715152155287818,m12:.11919202588130297,m20:.18043748326639894,m21:.07217499330655958,m22:.9503040785363679},MtxXYZ2RGB:{m00:3.2404541621141045,m01:-.9692660305051868,m02:.055643430959114726,m10:-1.5371385127977166,m11:1.8760108454466942,m12:-.2040259135167538,m20:-.498531409556016,m21:.041556017530349834,m22:1.0572251882231791},As:.9414285350000001,Bs:1.040417467,Cs:1.089532651,MtxAdaptMa:{m00:.8951,m01:-.7502,m02:.0389,m10:.2664,m11:1.7135,m12:-.0685,m20:-.1614,m21:.0367,m22:1.0296},MtxAdaptMaI:{m00:.9869929054667123,m01:.43230526972339456,m02:-.008528664575177328,m10:-.14705425642099013,m11:.5183602715367776,m12:.04004282165408487,m20:.15996265166373125,m21:.0492912282128556,m22:.9684866957875502}},se=new Map([[`a`,[1.0985,.35585]],[`b`,[1.0985,.35585]],[`c`,[.98074,1.18232]],[`d50`,[.96422,.82521]],[`d55`,[.95682,.92149]],[`d65`,[.95047,1.08883]],[`e`,[1,1,1]],[`f2`,[.99186,.67393]],[`f7`,[.95041,1.08747]],[`f11`,[1.00962,.6435]],[`icc`,[.96422,.82521]]]);function M(e){let t=se.get(String(e).toLowerCase());if(!t)throw Error(`unknown Lab illuminant `+e);j.labWhitePoint=e,j.Xn=t[0],j.Zn=t[1]}function ce(){return j.labWhitePoint}var N=(...e)=>{e=g(e,`lab`);let[t,n,r]=e,[i,a,o]=le(t,n,r),[s,c,l]=de(i,a,o);return[s,c,l,e.length>3?e[3]:1]},le=(e,t,n)=>{let{kE:r,kK:i,kKE:a,Xn:o,Yn:s,Zn:c}=j,l=(e+16)/116,u=.002*t+l,d=l-.005*n,f=u*u*u,p=d*d*d,m=f>r?f:(116*u-16)/i,h=e>a?((e+16)/116)**3:e/i,g=p>r?p:(116*d-16)/i;return[m*o,h*s,g*c]},ue=e=>{let t=Math.sign(e);return e=Math.abs(e),(e<=.0031308?e*12.92:1.055*e**(1/2.4)-.055)*t},de=(e,t,n)=>{let{MtxAdaptMa:r,MtxAdaptMaI:i,MtxXYZ2RGB:a,RefWhiteRGB:o,Xn:s,Yn:c,Zn:l}=j,u=s*r.m00+c*r.m10+l*r.m20,d=s*r.m01+c*r.m11+l*r.m21,f=s*r.m02+c*r.m12+l*r.m22,p=o.X*r.m00+o.Y*r.m10+o.Z*r.m20,m=o.X*r.m01+o.Y*r.m11+o.Z*r.m21,h=o.X*r.m02+o.Y*r.m12+o.Z*r.m22,g=(e*r.m00+t*r.m10+n*r.m20)*(p/u),_=(e*r.m01+t*r.m11+n*r.m21)*(m/d),v=(e*r.m02+t*r.m12+n*r.m22)*(h/f),y=g*i.m00+_*i.m10+v*i.m20,b=g*i.m01+_*i.m11+v*i.m21,x=g*i.m02+_*i.m12+v*i.m22,S=ue(y*a.m00+b*a.m10+x*a.m20),C=ue(y*a.m01+b*a.m11+x*a.m21),w=ue(y*a.m02+b*a.m12+x*a.m22);return[S*255,C*255,w*255]},fe=(...e)=>{let[t,n,r,...i]=g(e,`rgb`),[a,o,s]=he(t,n,r),[c,l,u]=pe(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]};function pe(e,t,n){let{Xn:r,Yn:i,Zn:a,kE:o,kK:s}=j,c=e/r,l=t/i,u=n/a,d=c>o?c**(1/3):(s*c+16)/116,f=l>o?l**(1/3):(s*l+16)/116,p=u>o?u**(1/3):(s*u+16)/116;return[116*f-16,500*(d-f),200*(f-p)]}function me(e){let t=Math.sign(e);return e=Math.abs(e),(e<=.04045?e/12.92:((e+.055)/1.055)**2.4)*t}var he=(e,t,n)=>{e=me(e/255),t=me(t/255),n=me(n/255);let{MtxRGB2XYZ:r,MtxAdaptMa:i,MtxAdaptMaI:a,Xn:o,Yn:s,Zn:c,As:l,Bs:u,Cs:d}=j,f=e*r.m00+t*r.m10+n*r.m20,p=e*r.m01+t*r.m11+n*r.m21,m=e*r.m02+t*r.m12+n*r.m22,h=o*i.m00+s*i.m10+c*i.m20,g=o*i.m01+s*i.m11+c*i.m21,_=o*i.m02+s*i.m12+c*i.m22,v=f*i.m00+p*i.m10+m*i.m20,y=f*i.m01+p*i.m11+m*i.m21,b=f*i.m02+p*i.m12+m*i.m22;return v*=h/l,y*=g/u,b*=_/d,f=v*a.m00+y*a.m10+b*a.m20,p=v*a.m01+y*a.m11+b*a.m21,m=v*a.m02+y*a.m12+b*a.m22,[f,p,m]};O.prototype.lab=function(){return fe(this._rgb)},Object.assign(k,{lab:(...e)=>new O(...e,`lab`),getLabWhitePoint:ce,setLabWhitePoint:M}),D.format.lab=N,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`lab`),h(e)===`array`&&e.length===3)return`lab`}}),O.prototype.darken=function(e=1){let t=this,n=t.lab();return n[0]-=j.Kn*e,new O(n,`lab`).alpha(t.alpha(),!0)},O.prototype.brighten=function(e=1){return this.darken(-e)},O.prototype.darker=O.prototype.darken,O.prototype.brighter=O.prototype.brighten,O.prototype.get=function(e){let[t,n]=e.split(`.`),r=this[t]();if(n){let e=t.indexOf(n)-(t.substr(0,2)===`ok`?2:0);if(e>-1)return r[e];throw Error(`unknown channel ${n} in mode ${t}`)}else return r};var{pow:ge}=Math,_e=1e-7,ve=20;O.prototype.luminance=function(e,t=`rgb`){if(e!==void 0&&h(e)===`number`){if(e===0)return new O([0,0,0,this._rgb[3]],`rgb`);if(e===1)return new O([255,255,255,this._rgb[3]],`rgb`);let n=this.luminance(),r=ve,i=(n,a)=>{let o=n.interpolate(a,.5,t),s=o.luminance();return Math.abs(e-s)<_e||!r--?o:s>e?i(n,o):i(o,a)};return new O([...(n>e?i(new O([0,0,0]),this):i(this,new O([255,255,255]))).rgb(),this._rgb[3]])}return ye(...this._rgb.slice(0,3))};var ye=(e,t,n)=>(e=be(e),t=be(t),n=be(n),.2126*e+.7152*t+.0722*n),be=e=>(e/=255,e<=.03928?e/12.92:ge((e+.055)/1.055,2.4)),P={},F=(e,t,n=.5,...r)=>{let i=r[0]||`lrgb`;if(!P[i]&&!r.length&&(i=Object.keys(P)[0]),!P[i])throw Error(`interpolation mode ${i} is not defined`);return h(e)!==`object`&&(e=new O(e)),h(t)!==`object`&&(t=new O(t)),P[i](e,t,n).alpha(e.alpha()+n*(t.alpha()-e.alpha()))};O.prototype.mix=O.prototype.interpolate=function(e,t=.5,...n){return F(this,e,t,...n)},O.prototype.premultiply=function(e=!1){let t=this._rgb,n=t[3];return e?(this._rgb=[t[0]*n,t[1]*n,t[2]*n,n],this):new O([t[0]*n,t[1]*n,t[2]*n,n],`rgb`)};var{sin:xe,cos:Se}=Math,Ce=(...e)=>{let[t,n,r]=g(e,`lch`);return isNaN(r)&&(r=0),r*=T,[t,Se(r)*n,xe(r)*n]},we=(...e)=>{e=g(e,`lch`);let[t,n,r]=e,[i,a,o]=Ce(t,n,r),[s,c,l]=N(i,a,o);return[s,c,l,e.length>3?e[3]:1]},Te=(...e)=>we(...E(g(e,`hcl`))),{sqrt:Ee,atan2:De,round:Oe}=Math,ke=(...e)=>{let[t,n,r]=g(e,`lab`),i=Ee(n*n+r*r),a=(De(r,n)*ee+360)%360;return Oe(i*1e4)===0&&(a=NaN),[t,i,a]},Ae=(...e)=>{let[t,n,r,...i]=g(e,`rgb`),[a,o,s]=fe(t,n,r),[c,l,u]=ke(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]};O.prototype.lch=function(){return Ae(this._rgb)},O.prototype.hcl=function(){return E(Ae(this._rgb))},Object.assign(k,{lch:(...e)=>new O(...e,`lch`),hcl:(...e)=>new O(...e,`hcl`)}),D.format.lch=we,D.format.hcl=Te,[`lch`,`hcl`].forEach(e=>D.autodetect.push({p:2,test:(...t)=>{if(t=g(t,e),h(t)===`array`&&t.length===3)return e}})),O.prototype.saturate=function(e=1){let t=this,n=t.lch();return n[1]+=j.Kn*e,n[1]<0&&(n[1]=0),new O(n,`lch`).alpha(t.alpha(),!0)},O.prototype.desaturate=function(e=1){return this.saturate(-e)},O.prototype.set=function(e,t,n=!1){let[r,i]=e.split(`.`),a=this[r]();if(i){let e=r.indexOf(i)-(r.substr(0,2)===`ok`?2:0);if(e>-1){if(h(t)==`string`)switch(t.charAt(0)){case`+`:a[e]+=+t;break;case`-`:a[e]+=+t;break;case`*`:a[e]*=+t.substr(1);break;case`/`:a[e]/=+t.substr(1);break;default:a[e]=+t}else if(h(t)===`number`)a[e]=t;else throw Error(`unsupported value for Color.set`);let i=new O(a,r);return n?(this._rgb=i._rgb,this):i}throw Error(`unknown channel ${i} in mode ${r}`)}else return a},O.prototype.tint=function(e=.5,...t){return F(this,`white`,e,...t)},O.prototype.shade=function(e=.5,...t){return F(this,`black`,e,...t)},P.rgb=(e,t,n)=>{let r=e._rgb,i=t._rgb;return new O(r[0]+n*(i[0]-r[0]),r[1]+n*(i[1]-r[1]),r[2]+n*(i[2]-r[2]),`rgb`)};var{sqrt:je,pow:Me}=Math;P.lrgb=(e,t,n)=>{let[r,i,a]=e._rgb,[o,s,c]=t._rgb;return new O(je(Me(r,2)*(1-n)+Me(o,2)*n),je(Me(i,2)*(1-n)+Me(s,2)*n),je(Me(a,2)*(1-n)+Me(c,2)*n),`rgb`)},P.lab=(e,t,n)=>{let r=e.lab(),i=t.lab();return new O(r[0]+n*(i[0]-r[0]),r[1]+n*(i[1]-r[1]),r[2]+n*(i[2]-r[2]),`lab`)};var Ne=(e,t,n,r)=>{let i,a;r===`hsl`?(i=e.hsl(),a=t.hsl()):r===`hsv`?(i=e.hsv(),a=t.hsv()):r===`hcg`?(i=e.hcg(),a=t.hcg()):r===`hsi`?(i=e.hsi(),a=t.hsi()):r===`lch`||r===`hcl`?(r=`hcl`,i=e.hcl(),a=t.hcl()):r===`oklch`&&(i=e.oklch().reverse(),a=t.oklch().reverse());let o,s,c,l,u,d;(r.substr(0,1)===`h`||r===`oklch`)&&([o,c,u]=i,[s,l,d]=a);let f,p,m,h;return!isNaN(o)&&!isNaN(s)?(h=s>o&&s-o>180?s-(o+360):s180?s+360-o:s-o,p=o+n*h):isNaN(o)?isNaN(s)?p=NaN:(p=s,(u==1||u==0)&&r!=`hsv`&&(f=l)):(p=o,(d==1||d==0)&&r!=`hsv`&&(f=c)),f===void 0&&(f=c+n*(l-c)),m=u+n*(d-u),r===`oklch`?new O([m,f,p],r):new O([p,f,m],r)},Pe=(e,t,n)=>Ne(e,t,n,`lch`);P.lch=Pe,P.hcl=Pe;var Fe=e=>{if(h(e)==`number`&&e>=0&&e<=16777215)return[e>>16,e>>8&255,e&255,1];throw Error(`unknown num color: `+e)},Ie=(...e)=>{let[t,n,r]=g(e,`rgb`);return(t<<16)+(n<<8)+r};O.prototype.num=function(){return Ie(this._rgb)},Object.assign(k,{num:(...e)=>new O(...e,`num`)}),D.format.num=Fe,D.autodetect.push({p:5,test:(...e)=>{if(e.length===1&&h(e[0])===`number`&&e[0]>=0&&e[0]<=16777215)return`num`}}),P.num=(e,t,n)=>{let r=e.num();return new O(r+n*(t.num()-r),`num`)};var{floor:Le}=Math,Re=(...e)=>{e=g(e,`hcg`);let[t,n,r]=e,i,a,o;r*=255;let s=n*255;if(n===0)i=a=o=r;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;let e=Le(t),c=t-e,l=r*(1-n),u=l+s*(1-c),d=l+s*c,f=l+s;switch(e){case 0:[i,a,o]=[f,d,l];break;case 1:[i,a,o]=[u,f,l];break;case 2:[i,a,o]=[l,f,d];break;case 3:[i,a,o]=[l,u,f];break;case 4:[i,a,o]=[d,l,f];break;case 5:[i,a,o]=[f,l,u];break}}return[i,a,o,e.length>3?e[3]:1]},ze=(...e)=>{let[t,n,r]=g(e,`rgb`),i=y(t,n,r),a=b(t,n,r),o=a-i,s=o*100/255,c=i/(255-o)*100,l;return o===0?l=NaN:(t===a&&(l=(n-r)/o),n===a&&(l=2+(r-t)/o),r===a&&(l=4+(t-n)/o),l*=60,l<0&&(l+=360)),[l,s,c]};O.prototype.hcg=function(){return ze(this._rgb)},k.hcg=(...e)=>new O(...e,`hcg`),D.format.hcg=Re,D.autodetect.push({p:1,test:(...e)=>{if(e=g(e,`hcg`),h(e)===`array`&&e.length===3)return`hcg`}}),P.hcg=(e,t,n)=>Ne(e,t,n,`hcg`);var{cos:Be}=Math,Ve=(...e)=>{e=g(e,`hsi`);let[t,n,r]=e,i,a,o;return isNaN(t)&&(t=0),isNaN(n)&&(n=0),t>360&&(t-=360),t<0&&(t+=360),t/=360,t<1/3?(o=(1-n)/3,i=(1+n*Be(C*t)/Be(w-C*t))/3,a=1-(o+i)):t<2/3?(t-=1/3,i=(1-n)/3,a=(1+n*Be(C*t)/Be(w-C*t))/3,o=1-(i+a)):(t-=2/3,a=(1-n)/3,o=(1+n*Be(C*t)/Be(w-C*t))/3,i=1-(a+o)),i=f(r*i*3),a=f(r*a*3),o=f(r*o*3),[i*255,a*255,o*255,e.length>3?e[3]:1]},{min:He,sqrt:Ue,acos:We}=Math,Ge=(...e)=>{let[t,n,r]=g(e,`rgb`);t/=255,n/=255,r/=255;let i,a=He(t,n,r),o=(t+n+r)/3,s=o>0?1-a/o:0;return s===0?i=NaN:(i=(t-n+(t-r))/2,i/=Ue((t-n)*(t-n)+(t-r)*(n-r)),i=We(i),r>n&&(i=C-i),i/=C),[i*360,s,o]};O.prototype.hsi=function(){return Ge(this._rgb)},k.hsi=(...e)=>new O(...e,`hsi`),D.format.hsi=Ve,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`hsi`),h(e)===`array`&&e.length===3)return`hsi`}}),P.hsi=(e,t,n)=>Ne(e,t,n,`hsi`);var Ke=(...e)=>{e=g(e,`hsl`);let[t,n,r]=e,i,a,o;if(n===0)i=a=o=r*255;else{let e=[0,0,0],s=[0,0,0],c=r<.5?r*(1+n):r+n-r*n,l=2*r-c,u=t/360;e[0]=u+1/3,e[1]=u,e[2]=u-1/3;for(let t=0;t<3;t++)e[t]<0&&(e[t]+=1),e[t]>1&&--e[t],6*e[t]<1?s[t]=l+(c-l)*6*e[t]:2*e[t]<1?s[t]=c:3*e[t]<2?s[t]=l+(c-l)*(2/3-e[t])*6:s[t]=l;[i,a,o]=[s[0]*255,s[1]*255,s[2]*255]}return e.length>3?[i,a,o,e[3]]:[i,a,o,1]},qe=(...e)=>{e=g(e,`rgba`);let[t,n,r]=e;t/=255,n/=255,r/=255;let i=y(t,n,r),a=b(t,n,r),o=(a+i)/2,s,c;return a===i?(s=0,c=NaN):s=o<.5?(a-i)/(a+i):(a-i)/(2-a-i),t==a?c=(n-r)/(a-i):n==a?c=2+(r-t)/(a-i):r==a&&(c=4+(t-n)/(a-i)),c*=60,c<0&&(c+=360),e.length>3&&e[3]!==void 0?[c,s,o,e[3]]:[c,s,o]};O.prototype.hsl=function(){return qe(this._rgb)},k.hsl=(...e)=>new O(...e,`hsl`),D.format.hsl=Ke,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`hsl`),h(e)===`array`&&e.length===3)return`hsl`}}),P.hsl=(e,t,n)=>Ne(e,t,n,`hsl`);var{floor:Je}=Math,Ye=(...e)=>{e=g(e,`hsv`);let[t,n,r]=e,i,a,o;if(r*=255,n===0)i=a=o=r;else{t===360&&(t=0),t>360&&(t-=360),t<0&&(t+=360),t/=60;let e=Je(t),s=t-e,c=r*(1-n),l=r*(1-n*s),u=r*(1-n*(1-s));switch(e){case 0:[i,a,o]=[r,u,c];break;case 1:[i,a,o]=[l,r,c];break;case 2:[i,a,o]=[c,r,u];break;case 3:[i,a,o]=[c,l,r];break;case 4:[i,a,o]=[u,c,r];break;case 5:[i,a,o]=[r,c,l];break}}return[i,a,o,e.length>3?e[3]:1]},{min:Xe,max:Ze}=Math,Qe=(...e)=>{e=g(e,`rgb`);let[t,n,r]=e,i=Xe(t,n,r),a=Ze(t,n,r),o=a-i,s,c,l;return l=a/255,a===0?(s=NaN,c=0):(c=o/a,t===a&&(s=(n-r)/o),n===a&&(s=2+(r-t)/o),r===a&&(s=4+(t-n)/o),s*=60,s<0&&(s+=360)),[s,c,l]};O.prototype.hsv=function(){return Qe(this._rgb)},k.hsv=(...e)=>new O(...e,`hsv`),D.format.hsv=Ye,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`hsv`),h(e)===`array`&&e.length===3)return`hsv`}}),P.hsv=(e,t,n)=>Ne(e,t,n,`hsv`);function $e(e,t){let n=e.length;Array.isArray(e[0])||(e=[e]),Array.isArray(t[0])||(t=t.map(e=>[e]));let r=t[0].length,i=t[0].map((e,n)=>t.map(e=>e[n])),a=e.map(e=>i.map(t=>Array.isArray(e)?e.reduce((e,n,r)=>e+n*(t[r]||0),0):t.reduce((t,n)=>t+n*e,0)));return n===1&&(a=a[0]),r===1?a.map(e=>e[0]):a}var et=(...e)=>{e=g(e,`lab`);let[t,n,r,...i]=e,[a,o,s]=tt([t,n,r]),[c,l,u]=de(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]};function tt(e){return $e([[1.2268798758459243,-.5578149944602171,.2813910456659647],[-.0405757452148008,1.112286803280317,-.0717110580655164],[-.0763729366746601,-.4214933324022432,1.5869240198367816]],$e([[1,.3963377773761749,.2158037573099136],[1,-.1055613458156586,-.0638541728258133],[1,-.0894841775298119,-1.2914855480194092]],e).map(e=>e**3))}var nt=(...e)=>{let[t,n,r,...i]=g(e,`rgb`);return[...rt(he(t,n,r)),...i.length>0&&i[0]<1?[i[0]]:[]]};function rt(e){return $e([[.210454268309314,.7936177747023054,-.0040720430116193],[1.9779985324311684,-2.42859224204858,.450593709617411],[.0259040424655478,.7827717124575296,-.8086757549230774]],$e([[.819022437996703,.3619062600528904,-.1288737815209879],[.0329836539323885,.9292868615863434,.0361446663506424],[.0481771893596242,.2642395317527308,.6335478284694309]],e).map(e=>Math.cbrt(e)))}O.prototype.oklab=function(){return nt(this._rgb)},Object.assign(k,{oklab:(...e)=>new O(...e,`oklab`)}),D.format.oklab=et,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`oklab`),h(e)===`array`&&e.length===3)return`oklab`}}),P.oklab=(e,t,n)=>{let r=e.oklab(),i=t.oklab();return new O(r[0]+n*(i[0]-r[0]),r[1]+n*(i[1]-r[1]),r[2]+n*(i[2]-r[2]),`oklab`)},P.oklch=(e,t,n)=>Ne(e,t,n,`oklch`);var{pow:it,sqrt:at,PI:ot,cos:st,sin:ct,atan2:lt}=Math,ut=(e,t=`lrgb`,n=null)=>{let r=e.length;n||=Array.from(Array(r)).map(()=>1);let i=r/n.reduce(function(e,t){return e+t});if(n.forEach((e,t)=>{n[t]*=i}),e=e.map(e=>new O(e)),t===`lrgb`)return dt(e,n);let a=e.shift(),o=a.get(t),s=[],c=0,l=0;for(let e=0;e{let i=e.get(t);u+=e.alpha()*n[r+1];for(let e=0;e=360;)t-=360;o[e]=t}else o[e]=o[e]/s[e];return u/=r,new O(o,t).alpha(u>.99999?1:u,!0)},dt=(e,t)=>{let n=e.length,r=[0,0,0,0];for(let i=0;i.9999999&&(r[3]=1),new O(p(r))},{pow:ft}=Math;function pt(e){let t=`rgb`,n=k(`#ccc`),r=0,i=[0,1],a=[0,1],o=[],s=[0,0],c=!1,l=[],u=!1,d=0,p=1,m=!1,g={},_=!0,v=1,y=function(e){if(e||=[`#fff`,`#000`],e&&h(e)===`string`&&k.brewer&&k.brewer[e.toLowerCase()]&&(e=k.brewer[e.toLowerCase()]),h(e)===`array`){e.length===1&&(e=[e[0],e[0]]),e=e.slice(0);for(let t=0;t=c[n];)n++;return n-1}return 0},x=e=>e,S=e=>e,C=function(e,r){let i,a;if(r??=!1,isNaN(e)||e===null)return n;a=r?e:c&&c.length>2?b(e)/(c.length-2):p===d?1:(e-d)/(p-d),a=S(a),r||(a=x(a)),v!==1&&(a=ft(a,v)),a=s[0]+a*(1-s[0]-s[1]),a=f(a,0,1);let u=Math.floor(a*1e4);if(_&&g[u])i=g[u];else{if(h(l)===`array`)for(let e=0;e=n&&e===o.length-1){i=l[e];break}if(a>n&&ag={};y(e);let T=function(e){let t=k(C(e));return u&&t[u]?t[u]():t};return T.classes=function(e){if(e!=null){if(h(e)===`array`)c=e,i=[e[0],e[e.length-1]];else{let t=k.analyze(i);c=e===0?[t.min,t.max]:k.limits(t,`e`,e)}return T}return c},T.domain=function(e){if(!arguments.length)return a;a=e.slice(0),d=e[0],p=e[e.length-1],o=[];let t=l.length;if(e.length===t&&d!==p)for(let t of Array.from(e))o.push((t-d)/(p-d));else{for(let e=0;e2){let t=e.map((t,n)=>n/(e.length-1)),n=e.map(e=>(e-d)/(p-d));n.every((e,n)=>t[n]===e)||(S=e=>{if(e<=0||e>=1)return e;let r=0;for(;e>=n[r+1];)r++;let i=(e-n[r])/(n[r+1]-n[r]);return t[r]+i*(t[r+1]-t[r])})}}return i=[d,p],T},T.mode=function(e){return arguments.length?(t=e,w(),T):t},T.range=function(e,t){return y(e,t),T},T.out=function(e){return u=e,T},T.spread=function(e){return arguments.length?(r=e,T):r},T.correctLightness=function(e){return e??=!0,m=e,w(),x=m?function(e){let t=C(0,!0).lab()[0],n=C(1,!0).lab()[0],r=t>n,i=C(e,!0).lab()[0],a=t+(n-t)*e,o=i-a,s=0,c=1,l=20;for(;Math.abs(o)>.01&&l-- >0;)(function(){return r&&(o*=-1),o<0?(s=e,e+=(c-e)*.5):(c=e,e+=(s-e)*.5),i=C(e,!0).lab()[0],o=i-a})();return e}:e=>e,T},T.padding=function(e){return e==null?s:(h(e)===`number`&&(e=[e,e]),s=e,T)},T.colors=function(t,n){arguments.length<2&&(n=`hex`);let r=[];if(arguments.length===0)r=l.slice(0);else if(t===1)r=[T(.5)];else if(t>1){let e=i[0],n=i[1]-e;r=mt(0,t,!1).map(r=>T(e+r/(t-1)*n))}else{e=[];let t=[];if(c&&c.length>2)for(let e=1,n=c.length,r=1<=n;r?en;r?e++:e--)t.push((c[e-1]+c[e])*.5);else t=i;r=t.map(e=>T(e))}return k[n]&&(r=r.map(e=>e[n]())),r},T.cache=function(e){return e==null?_:(_=e,T)},T.gamma=function(e){return e==null?v:(v=e,T)},T.nodata=function(e){return e==null?n:(n=k(e),T)},T}function mt(e,t,n){let r=[],i=ea;i?t++:t--)r.push(t);return r}var ht=function(e){let t=[1,1];for(let n=1;nnew O(e)),e.length===2)[n,r]=e.map(e=>e.lab()),t=function(e){return new O([0,1,2].map(t=>n[t]+e*(r[t]-n[t])),`lab`)};else if(e.length===3)[n,r,i]=e.map(e=>e.lab()),t=function(e){return new O([0,1,2].map(t=>(1-e)*(1-e)*n[t]+2*(1-e)*e*r[t]+e*e*i[t]),`lab`)};else if(e.length===4){let a;[n,r,i,a]=e.map(e=>e.lab()),t=function(e){return new O([0,1,2].map(t=>(1-e)*(1-e)*(1-e)*n[t]+3*(1-e)*(1-e)*e*r[t]+3*(1-e)*e*e*i[t]+e*e*e*a[t]),`lab`)}}else if(e.length>=5){let n,r,i;n=e.map(e=>e.lab()),i=e.length-1,r=ht(i),t=function(e){let t=1-e;return new O([0,1,2].map(a=>n.reduce((n,o,s)=>n+r[s]*t**(i-s)*e**s*o[a],0)),`lab`)}}else throw RangeError(`No point in running bezier with only one color.`);return t},_t=e=>{let t=gt(e);return t.scale=()=>pt(t),t},{round:vt}=Math;O.prototype.rgb=function(e=!0){return e===!1?this._rgb.slice(0,3):this._rgb.slice(0,3).map(vt)},O.prototype.rgba=function(e=!0){return this._rgb.slice(0,4).map((t,n)=>n<3?e===!1?t:vt(t):t)},Object.assign(k,{rgb:(...e)=>new O(...e,`rgb`)}),D.format.rgb=(...e)=>{let t=g(e,`rgba`);return t[3]===void 0&&(t[3]=1),t},D.autodetect.push({p:3,test:(...e)=>{if(e=g(e,`rgba`),h(e)===`array`&&(e.length===3||e.length===4&&h(e[3])==`number`&&e[3]>=0&&e[3]<=1))return`rgb`}});var I=(e,t,n)=>{if(!I[n])throw Error(`unknown blend mode `+n);return I[n](e,t)},L=e=>(t,n)=>{let r=k(n).rgb(),i=k(t).rgb();return k.rgb(e(r,i))},R=e=>(t,n)=>{let r=[];return r[0]=e(t[0],n[0]),r[1]=e(t[1],n[1]),r[2]=e(t[2],n[2]),r};I.normal=L(R(e=>e)),I.multiply=L(R((e,t)=>e*t/255)),I.screen=L(R((e,t)=>255*(1-(1-e/255)*(1-t/255)))),I.overlay=L(R((e,t)=>t<128?2*e*t/255:255*(1-2*(1-e/255)*(1-t/255)))),I.darken=L(R((e,t)=>e>t?t:e)),I.lighten=L(R((e,t)=>e>t?e:t)),I.dodge=L(R((e,t)=>e===255?255:(e=t/255*255/(1-e/255),e>255?255:e))),I.burn=L(R((e,t)=>255*(1-(1-t/255)/(e/255))));var{pow:yt,sin:bt,cos:xt}=Math;function St(e=300,t=-1.5,n=1,r=1,i=[0,1]){let a=0,o;h(i)===`array`?o=i[1]-i[0]:(o=0,i=[i,i]);let s=function(s){let c=C*((e+120)/360+t*s),l=yt(i[0]+o*s,r),u=(a===0?n:n[0]+s*a)*l*(1-l)/2,d=xt(c),f=bt(c),m=l+u*(-.14861*d+1.78277*f),h=l+u*(-.29227*d-.90649*f),g=l+1.97294*d*u;return k(p([m*255,h*255,g*255,1]))};return s.start=function(t){return t==null?e:(e=t,s)},s.rotations=function(e){return e==null?t:(t=e,s)},s.gamma=function(e){return e==null?r:(r=e,s)},s.hue=function(e){return e==null?n:(n=e,h(n)===`array`?(a=n[1]-n[0],a===0&&(n=n[1])):a=0,s)},s.lightness=function(e){return e==null?i:(h(e)===`array`?(i=e,o=e[1]-e[0]):(i=[e,e],o=0),s)},s.scale=()=>k.scale(s),s.hue(n),s}var Ct=`0123456789abcdef`,{floor:wt,random:Tt}=Math,Et=(e=Tt)=>{let t=`#`;for(let n=0;n<6;n++)t+=Ct.charAt(wt(e()*16));return new O(t,`hex`)},{log:Dt,pow:Ot,floor:kt,abs:At}=Math;function jt(e,t=null){let n={min:Number.MAX_VALUE,max:Number.MAX_VALUE*-1,sum:0,values:[],count:0};return h(e)===`object`&&(e=Object.values(e)),e.forEach(e=>{t&&h(e)===`object`&&(e=e[t]),e!=null&&!isNaN(e)&&(n.values.push(e),n.sum+=e,en.max&&(n.max=e),n.count+=1)}),n.domain=[n.min,n.max],n.limits=(e,t)=>Mt(n,e,t),n}function Mt(e,t=`equal`,n=7){h(e)==`array`&&(e=jt(e));let{min:r,max:i}=e,a=e.values.sort((e,t)=>e-t);if(n===1)return[r,i];let o=[];if(t.substr(0,1)===`c`&&(o.push(r),o.push(i)),t.substr(0,1)===`e`){o.push(r);for(let e=1;e 0`);let e=Math.LOG10E*Dt(r),t=Math.LOG10E*Dt(i);o.push(r);for(let r=1;r200&&(l=!1)}let f={};for(let e=0;ee-t),o.push(p[0]);for(let e=1;e{e=new O(e),t=new O(t);let n=e.luminance(),r=t.luminance();return n>r?(n+.05)/(r+.05):(r+.05)/(n+.05)},Pt=.027,Ft=5e-4,It=.1,Lt=1.14,Rt=.022,zt=1.414,Bt=(e,t)=>{e=new O(e),t=new O(t),e.alpha()<1&&(e=F(t,e,e.alpha(),`rgb`));let n=Vt(...e.rgb()),r=Vt(...t.rgb()),i=n>=Rt?n:n+(Rt-n)**+zt,a=r>=Rt?r:r+(Rt-r)**+zt,o=a**.56-i**.57,s=a**.65-i**.62,c=Math.abs(a-i)0?c-Pt:c+Pt)*100};function Vt(e,t,n){return .2126729*(e/255)**2.4+.7151522*(t/255)**2.4+.072175*(n/255)**2.4}var{sqrt:z,pow:B,min:Ht,max:Ut,atan2:Wt,abs:Gt,cos:Kt,sin:qt,exp:Jt,PI:Yt}=Math;function Xt(e,t,n=1,r=1,i=1){var a=function(e){return 360*e/(2*Yt)},o=function(e){return 2*Yt*e/360};e=new O(e),t=new O(t);let[s,c,l]=Array.from(e.lab()),[u,d,f]=Array.from(t.lab()),p=(s+u)/2,m=(z(B(c,2)+B(l,2))+z(B(d,2)+B(f,2)))/2,h=.5*(1-z(B(m,7)/(B(m,7)+B(25,7)))),g=c*(1+h),_=d*(1+h),v=z(B(g,2)+B(l,2)),y=z(B(_,2)+B(f,2)),b=(v+y)/2,x=a(Wt(l,g)),S=a(Wt(f,_)),C=x>=0?x:x+360,w=S>=0?S:S+360,T=Gt(C-w)>180?(C+w+360)/2:(C+w)/2,ee=1-.17*Kt(o(T-30))+.24*Kt(o(2*T))+.32*Kt(o(3*T+6))-.2*Kt(o(4*T-63)),E=w-C;E=Gt(E)<=180?E:w<=C?E+360:E-360,E=2*z(v*y)*qt(o(E)/2);let D=u-s,te=y-v,k=1+.015*B(p-50,2)/z(20+B(p-50,2)),A=1+.045*b,ne=1+.015*b*ee,re=30*Jt(-B((T-275)/25,2)),ie=-(2*z(B(b,7)/(B(b,7)+B(25,7))))*qt(2*o(re));return Ut(0,Ht(100,z(B(D/(n*k),2)+B(te/(r*A),2)+B(E/(i*ne),2)+ie*(te/(r*A))*(E/(i*ne)))))}function Zt(e,t,n=`lab`){e=new O(e),t=new O(t);let r=e.get(n),i=t.get(n),a=0;for(let e in r){let t=(r[e]||0)-(i[e]||0);a+=t*t}return Math.sqrt(a)}var Qt=(...e)=>{try{return new O(...e),!0}catch{return!1}},$t={cool(){return pt([k.hsl(180,1,.9),k.hsl(250,.7,.4)])},hot(){return pt([`#000`,`#f00`,`#ff0`,`#fff`],[0,.25,.75,1]).mode(`rgb`)}},en={OrRd:[`#fff7ec`,`#fee8c8`,`#fdd49e`,`#fdbb84`,`#fc8d59`,`#ef6548`,`#d7301f`,`#b30000`,`#7f0000`],PuBu:[`#fff7fb`,`#ece7f2`,`#d0d1e6`,`#a6bddb`,`#74a9cf`,`#3690c0`,`#0570b0`,`#045a8d`,`#023858`],BuPu:[`#f7fcfd`,`#e0ecf4`,`#bfd3e6`,`#9ebcda`,`#8c96c6`,`#8c6bb1`,`#88419d`,`#810f7c`,`#4d004b`],Oranges:[`#fff5eb`,`#fee6ce`,`#fdd0a2`,`#fdae6b`,`#fd8d3c`,`#f16913`,`#d94801`,`#a63603`,`#7f2704`],BuGn:[`#f7fcfd`,`#e5f5f9`,`#ccece6`,`#99d8c9`,`#66c2a4`,`#41ae76`,`#238b45`,`#006d2c`,`#00441b`],YlOrBr:[`#ffffe5`,`#fff7bc`,`#fee391`,`#fec44f`,`#fe9929`,`#ec7014`,`#cc4c02`,`#993404`,`#662506`],YlGn:[`#ffffe5`,`#f7fcb9`,`#d9f0a3`,`#addd8e`,`#78c679`,`#41ab5d`,`#238443`,`#006837`,`#004529`],Reds:[`#fff5f0`,`#fee0d2`,`#fcbba1`,`#fc9272`,`#fb6a4a`,`#ef3b2c`,`#cb181d`,`#a50f15`,`#67000d`],RdPu:[`#fff7f3`,`#fde0dd`,`#fcc5c0`,`#fa9fb5`,`#f768a1`,`#dd3497`,`#ae017e`,`#7a0177`,`#49006a`],Greens:[`#f7fcf5`,`#e5f5e0`,`#c7e9c0`,`#a1d99b`,`#74c476`,`#41ab5d`,`#238b45`,`#006d2c`,`#00441b`],YlGnBu:[`#ffffd9`,`#edf8b1`,`#c7e9b4`,`#7fcdbb`,`#41b6c4`,`#1d91c0`,`#225ea8`,`#253494`,`#081d58`],Purples:[`#fcfbfd`,`#efedf5`,`#dadaeb`,`#bcbddc`,`#9e9ac8`,`#807dba`,`#6a51a3`,`#54278f`,`#3f007d`],GnBu:[`#f7fcf0`,`#e0f3db`,`#ccebc5`,`#a8ddb5`,`#7bccc4`,`#4eb3d3`,`#2b8cbe`,`#0868ac`,`#084081`],Greys:[`#ffffff`,`#f0f0f0`,`#d9d9d9`,`#bdbdbd`,`#969696`,`#737373`,`#525252`,`#252525`,`#000000`],YlOrRd:[`#ffffcc`,`#ffeda0`,`#fed976`,`#feb24c`,`#fd8d3c`,`#fc4e2a`,`#e31a1c`,`#bd0026`,`#800026`],PuRd:[`#f7f4f9`,`#e7e1ef`,`#d4b9da`,`#c994c7`,`#df65b0`,`#e7298a`,`#ce1256`,`#980043`,`#67001f`],Blues:[`#f7fbff`,`#deebf7`,`#c6dbef`,`#9ecae1`,`#6baed6`,`#4292c6`,`#2171b5`,`#08519c`,`#08306b`],PuBuGn:[`#fff7fb`,`#ece2f0`,`#d0d1e6`,`#a6bddb`,`#67a9cf`,`#3690c0`,`#02818a`,`#016c59`,`#014636`],Viridis:[`#440154`,`#482777`,`#3f4a8a`,`#31678e`,`#26838f`,`#1f9d8a`,`#6cce5a`,`#b6de2b`,`#fee825`],Spectral:[`#9e0142`,`#d53e4f`,`#f46d43`,`#fdae61`,`#fee08b`,`#ffffbf`,`#e6f598`,`#abdda4`,`#66c2a5`,`#3288bd`,`#5e4fa2`],RdYlGn:[`#a50026`,`#d73027`,`#f46d43`,`#fdae61`,`#fee08b`,`#ffffbf`,`#d9ef8b`,`#a6d96a`,`#66bd63`,`#1a9850`,`#006837`],RdBu:[`#67001f`,`#b2182b`,`#d6604d`,`#f4a582`,`#fddbc7`,`#f7f7f7`,`#d1e5f0`,`#92c5de`,`#4393c3`,`#2166ac`,`#053061`],PiYG:[`#8e0152`,`#c51b7d`,`#de77ae`,`#f1b6da`,`#fde0ef`,`#f7f7f7`,`#e6f5d0`,`#b8e186`,`#7fbc41`,`#4d9221`,`#276419`],PRGn:[`#40004b`,`#762a83`,`#9970ab`,`#c2a5cf`,`#e7d4e8`,`#f7f7f7`,`#d9f0d3`,`#a6dba0`,`#5aae61`,`#1b7837`,`#00441b`],RdYlBu:[`#a50026`,`#d73027`,`#f46d43`,`#fdae61`,`#fee090`,`#ffffbf`,`#e0f3f8`,`#abd9e9`,`#74add1`,`#4575b4`,`#313695`],BrBG:[`#543005`,`#8c510a`,`#bf812d`,`#dfc27d`,`#f6e8c3`,`#f5f5f5`,`#c7eae5`,`#80cdc1`,`#35978f`,`#01665e`,`#003c30`],RdGy:[`#67001f`,`#b2182b`,`#d6604d`,`#f4a582`,`#fddbc7`,`#ffffff`,`#e0e0e0`,`#bababa`,`#878787`,`#4d4d4d`,`#1a1a1a`],PuOr:[`#7f3b08`,`#b35806`,`#e08214`,`#fdb863`,`#fee0b6`,`#f7f7f7`,`#d8daeb`,`#b2abd2`,`#8073ac`,`#542788`,`#2d004b`],Set2:[`#66c2a5`,`#fc8d62`,`#8da0cb`,`#e78ac3`,`#a6d854`,`#ffd92f`,`#e5c494`,`#b3b3b3`],Accent:[`#7fc97f`,`#beaed4`,`#fdc086`,`#ffff99`,`#386cb0`,`#f0027f`,`#bf5b17`,`#666666`],Set1:[`#e41a1c`,`#377eb8`,`#4daf4a`,`#984ea3`,`#ff7f00`,`#ffff33`,`#a65628`,`#f781bf`,`#999999`],Set3:[`#8dd3c7`,`#ffffb3`,`#bebada`,`#fb8072`,`#80b1d3`,`#fdb462`,`#b3de69`,`#fccde5`,`#d9d9d9`,`#bc80bd`,`#ccebc5`,`#ffed6f`],Dark2:[`#1b9e77`,`#d95f02`,`#7570b3`,`#e7298a`,`#66a61e`,`#e6ab02`,`#a6761d`,`#666666`],Paired:[`#a6cee3`,`#1f78b4`,`#b2df8a`,`#33a02c`,`#fb9a99`,`#e31a1c`,`#fdbf6f`,`#ff7f00`,`#cab2d6`,`#6a3d9a`,`#ffff99`,`#b15928`],Pastel2:[`#b3e2cd`,`#fdcdac`,`#cbd5e8`,`#f4cae4`,`#e6f5c9`,`#fff2ae`,`#f1e2cc`,`#cccccc`],Pastel1:[`#fbb4ae`,`#b3cde3`,`#ccebc5`,`#decbe4`,`#fed9a6`,`#ffffcc`,`#e5d8bd`,`#fddaec`,`#f2f2f2`]},tn=Object.keys(en),nn=new Map(tn.map(e=>[e.toLowerCase(),e])),rn=typeof Proxy==`function`?new Proxy(en,{get(e,t){let n=t.toLowerCase();if(nn.has(n))return e[nn.get(n)]},getOwnPropertyNames(){return Object.getOwnPropertyNames(tn)}}):en,an=(...e)=>{e=g(e,`cmyk`);let[t,n,r,i]=e,a=e.length>4?e[4]:1;return i===1?[0,0,0,a]:[t>=1?0:255*(1-t)*(1-i),n>=1?0:255*(1-n)*(1-i),r>=1?0:255*(1-r)*(1-i),a]},{max:on}=Math,sn=(...e)=>{let[t,n,r]=g(e,`rgb`);t/=255,n/=255,r/=255;let i=1-on(t,on(n,r)),a=i<1?1/(1-i):0;return[(1-t-i)*a,(1-n-i)*a,(1-r-i)*a,i]};O.prototype.cmyk=function(){return sn(this._rgb)},Object.assign(k,{cmyk:(...e)=>new O(...e,`cmyk`)}),D.format.cmyk=an,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`cmyk`),h(e)===`array`&&e.length===4)return`cmyk`}});var cn=(...e)=>{let t=g(e,`hsla`),n=_(e)||`lsa`;return t[0]=x(t[0]||0)+`deg`,t[1]=x(t[1]*100)+`%`,t[2]=x(t[2]*100)+`%`,n===`hsla`||t.length>3&&t[3]<1?(t[3]=`/ `+(t.length>3?t[3]:1),n=`hsla`):t.length=3,`${n.substr(0,3)}(${t.join(` `)})`},ln=(...e)=>{let t=g(e,`lab`),n=_(e)||`lab`;return t[0]=x(t[0])+`%`,t[1]=x(t[1]),t[2]=x(t[2]),n===`laba`||t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`lab(${t.join(` `)})`},un=(...e)=>{let t=g(e,`lch`),n=_(e)||`lab`;return t[0]=x(t[0])+`%`,t[1]=x(t[1]),t[2]=isNaN(t[2])?`none`:x(t[2])+`deg`,n===`lcha`||t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`lch(${t.join(` `)})`},dn=(...e)=>{let t=g(e,`lab`);return t[0]=x(t[0]*100)+`%`,t[1]=S(t[1]),t[2]=S(t[2]),t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`oklab(${t.join(` `)})`},fn=(...e)=>{let[t,n,r,...i]=g(e,`rgb`),[a,o,s]=nt(t,n,r),[c,l,u]=ke(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]},pn=(...e)=>{let t=g(e,`lch`);return t[0]=x(t[0]*100)+`%`,t[1]=S(t[1]),t[2]=isNaN(t[2])?`none`:x(t[2])+`deg`,t.length>3&&t[3]<1?t[3]=`/ `+(t.length>3?t[3]:1):t.length=3,`oklch(${t.join(` `)})`},{round:mn}=Math,hn=(...e)=>{let t=g(e,`rgba`),n=_(e)||`rgb`;if(n.substr(0,3)===`hsl`)return cn(qe(t),n);if(n.substr(0,3)===`lab`){let e=ce();M(`d50`);let r=ln(fe(t),n);return M(e),r}if(n.substr(0,3)===`lch`){let e=ce();M(`d50`);let r=un(Ae(t),n);return M(e),r}return n.substr(0,5)===`oklab`?dn(nt(t)):n.substr(0,5)===`oklch`?pn(fn(t)):(t[0]=mn(t[0]),t[1]=mn(t[1]),t[2]=mn(t[2]),(n===`rgba`||t.length>3&&t[3]<1)&&(t[3]=`/ `+(t.length>3?t[3]:1),n=`rgba`),`${n.substr(0,3)}(${t.slice(0,n===`rgb`?3:4).join(` `)})`)},gn=(...e)=>{e=g(e,`lch`);let[t,n,r,...i]=e,[a,o,s]=Ce(t,n,r),[c,l,u]=et(a,o,s);return[c,l,u,...i.length>0&&i[0]<1?[i[0]]:[]]},V=`((?:-?\\d+)|(?:-?\\d+(?:\\.\\d+)?)%|none)`,H=`((?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)%?)|none)`,_n=`((?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)%)|none)`,U=`\\s*`,vn=`\\s+`,yn=`\\s*,\\s*`,bn=`((?:-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:deg)?)|none)`,xn=`\\s*(?:\\/\\s*((?:[01]|[01]?\\.\\d+)|\\d+(?:\\.\\d+)?%))?`,Sn=RegExp(`^rgba?\\(`+U+[V,V,V].join(vn)+xn+`\\)$`),Cn=RegExp(`^rgb\\(`+U+[V,V,V].join(yn)+U+`\\)$`),wn=RegExp(`^rgba\\(`+U+[V,V,V,H].join(yn)+U+`\\)$`),Tn=RegExp(`^hsla?\\(`+U+[bn,_n,_n].join(vn)+xn+`\\)$`),En=RegExp(`^hsl?\\(`+U+[bn,_n,_n].join(yn)+U+`\\)$`),Dn=/^hsla\(\s*(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)%\s*,\s*(-?\d+(?:\.\d+)?)%\s*,\s*([01]|[01]?\.\d+)\)$/,On=RegExp(`^lab\\(`+U+[H,H,H].join(vn)+xn+`\\)$`),kn=RegExp(`^lch\\(`+U+[H,H,bn].join(vn)+xn+`\\)$`),An=RegExp(`^oklab\\(`+U+[H,H,H].join(vn)+xn+`\\)$`),jn=RegExp(`^oklch\\(`+U+[H,H,bn].join(vn)+xn+`\\)$`),{round:Mn}=Math,Nn=e=>e.map((e,t)=>t<=2?f(Mn(e),0,255):e),W=(e,t=0,n=100,r=!1)=>(typeof e==`string`&&e.endsWith(`%`)&&(e=parseFloat(e.substring(0,e.length-1))/100,e=r?t+(e+1)*.5*(n-t):t+e*(n-t)),+e),G=(e,t)=>e===`none`?t:e,Pn=e=>{if(e=e.toLowerCase().trim(),e===`transparent`)return[0,0,0,0];let t;if(D.format.named)try{return D.format.named(e)}catch{}if((t=e.match(Sn))||(t=e.match(Cn))){let e=t.slice(1,4);for(let t=0;t<3;t++)e[t]=+W(G(e[t],0),0,255);e=Nn(e);let n=t[4]===void 0?1:+W(t[4],0,1);return e[3]=n,e}if(t=e.match(wn)){let e=t.slice(1,5);for(let t=0;t<4;t++)e[t]=+W(e[t],0,255);return e}if((t=e.match(Tn))||(t=e.match(En))){let e=t.slice(1,4);e[0]=+G(e[0].replace(`deg`,``),0),e[1]=W(G(e[1],0),0,100)*.01,e[2]=W(G(e[2],0),0,100)*.01;let n=Nn(Ke(e));return n[3]=t[4]===void 0?1:+W(t[4],0,1),n}if(t=e.match(Dn)){let e=t.slice(1,4);e[1]*=.01,e[2]*=.01;let n=Ke(e);for(let e=0;e<3;e++)n[e]=Mn(n[e]);return n[3]=+t[4],n}if(t=e.match(On)){let e=t.slice(1,4);e[0]=W(G(e[0],0),0,100),e[1]=W(G(e[1],0),-125,125,!0),e[2]=W(G(e[2],0),-125,125,!0);let n=ce();M(`d50`);let r=Nn(N(e));return M(n),r[3]=t[4]===void 0?1:+W(t[4],0,1),r}if(t=e.match(kn)){let e=t.slice(1,4);e[0]=W(e[0],0,100),e[1]=W(G(e[1],0),0,150,!1),e[2]=+G(e[2].replace(`deg`,``),0);let n=ce();M(`d50`);let r=Nn(we(e));return M(n),r[3]=t[4]===void 0?1:+W(t[4],0,1),r}if(t=e.match(An)){let e=t.slice(1,4);e[0]=W(G(e[0],0),0,1),e[1]=W(G(e[1],0),-.4,.4,!0),e[2]=W(G(e[2],0),-.4,.4,!0);let n=Nn(et(e));return n[3]=t[4]===void 0?1:+W(t[4],0,1),n}if(t=e.match(jn)){let e=t.slice(1,4);e[0]=W(G(e[0],0),0,1),e[1]=W(G(e[1],0),0,.4,!1),e[2]=+G(e[2].replace(`deg`,``),0);let n=Nn(gn(e));return n[3]=t[4]===void 0?1:+W(t[4],0,1),n}};Pn.test=e=>Sn.test(e)||Tn.test(e)||On.test(e)||kn.test(e)||An.test(e)||jn.test(e)||Cn.test(e)||wn.test(e)||En.test(e)||Dn.test(e)||e===`transparent`,O.prototype.css=function(e){return hn(this._rgb,e)},k.css=(...e)=>new O(...e,`css`),D.format.css=Pn,D.autodetect.push({p:5,test:(e,...t)=>{if(!t.length&&h(e)===`string`&&Pn.test(e))return`css`}}),D.format.gl=(...e)=>{let t=g(e,`rgba`);return t[0]*=255,t[1]*=255,t[2]*=255,t},k.gl=(...e)=>new O(...e,`gl`),O.prototype.gl=function(){let e=this._rgb;return[e[0]/255,e[1]/255,e[2]/255,e[3]]},O.prototype.hex=function(e){return oe(this._rgb,e)},k.hex=(...e)=>new O(...e,`hex`),D.format.hex=ie,D.autodetect.push({p:4,test:(e,...t)=>{if(!t.length&&h(e)===`string`&&[3,4,5,6,7,8,9].indexOf(e.length)>=0)return`hex`}});var{log:Fn}=Math,In=e=>{let t=e/100,n,r,i;return t<66?(n=255,r=t<6?0:-155.25485562709179-.44596950469579133*(r=t-2)+104.49216199393888*Fn(r),i=t<20?0:-254.76935184120902+.8274096064007395*(i=t-10)+115.67994401066147*Fn(i)):(n=351.97690566805693+.114206453784165*(n=t-55)-40.25366309332127*Fn(n),r=325.4494125711974+.07943456536662342*(r=t-50)-28.0852963507957*Fn(r),i=255),[n,r,i,1]},{round:Ln}=Math,Rn=(...e)=>{let t=g(e,`rgb`),n=t[0],r=t[2],i=1e3,a=4e4,o;for(;a-i>.4;){o=(a+i)*.5;let e=In(o);e[2]/e[0]>=r/n?a=o:i=o}return Ln(o)};O.prototype.temp=O.prototype.kelvin=O.prototype.temperature=function(){return Rn(this._rgb)};var zn=(...e)=>new O(...e,`temp`);Object.assign(k,{temp:zn,kelvin:zn,temperature:zn}),D.format.temp=D.format.kelvin=D.format.temperature=In,O.prototype.oklch=function(){return fn(this._rgb)},Object.assign(k,{oklch:(...e)=>new O(...e,`oklch`)}),D.format.oklch=gn,D.autodetect.push({p:2,test:(...e)=>{if(e=g(e,`oklch`),h(e)===`array`&&e.length===3)return`oklch`}}),Object.assign(k,{analyze:jt,average:ut,bezier:_t,blend:I,brewer:rn,Color:O,colors:A,contrast:Nt,contrastAPCA:Bt,cubehelix:St,deltaE:Xt,distance:Zt,input:D,interpolate:F,limits:Mt,mix:F,random:Et,scale:pt,scales:$t,valid:Qt});var K=k,q=class e{constructor(){this.hex=`#000000`,this.rgb_r=0,this.rgb_g=0,this.rgb_b=0,this.xyz_x=0,this.xyz_y=0,this.xyz_z=0,this.luv_l=0,this.luv_u=0,this.luv_v=0,this.lch_l=0,this.lch_c=0,this.lch_h=0,this.hsluv_h=0,this.hsluv_s=0,this.hsluv_l=0,this.hpluv_h=0,this.hpluv_p=0,this.hpluv_l=0,this.r0s=0,this.r0i=0,this.r1s=0,this.r1i=0,this.g0s=0,this.g0i=0,this.g1s=0,this.g1i=0,this.b0s=0,this.b0i=0,this.b1s=0,this.b1i=0}static fromLinear(e){return e<=.0031308?12.92*e:1.055*e**(1/2.4)-.055}static toLinear(e){return e>.04045?((e+.055)/1.055)**2.4:e/12.92}static yToL(t){return t<=e.epsilon?t/e.refY*e.kappa:116*(t/e.refY)**(1/3)-16}static lToY(t){return t<=8?e.refY*t/e.kappa:e.refY*((t+16)/116)**3}static rgbChannelToHex(t){let n=Math.round(t*255),r=n%16,i=(n-r)/16|0;return e.hexChars.charAt(i)+e.hexChars.charAt(r)}static hexToRgbChannel(t,n){let r=e.hexChars.indexOf(t.charAt(n)),i=e.hexChars.indexOf(t.charAt(n+1));return(r*16+i)/255}static distanceFromOriginAngle(e,t,n){let r=t/(Math.sin(n)-e*Math.cos(n));return r<0?1/0:r}static distanceFromOrigin(e,t){return Math.abs(t)/Math.sqrt(e**2+1)}static min6(e,t,n,r,i,a){return Math.min(e,Math.min(t,Math.min(n,Math.min(r,Math.min(i,a)))))}rgbToHex(){this.hex=`#`,this.hex+=e.rgbChannelToHex(this.rgb_r),this.hex+=e.rgbChannelToHex(this.rgb_g),this.hex+=e.rgbChannelToHex(this.rgb_b)}hexToRgb(){this.hex=this.hex.toLowerCase(),this.rgb_r=e.hexToRgbChannel(this.hex,1),this.rgb_g=e.hexToRgbChannel(this.hex,3),this.rgb_b=e.hexToRgbChannel(this.hex,5)}xyzToRgb(){this.rgb_r=e.fromLinear(e.m_r0*this.xyz_x+e.m_r1*this.xyz_y+e.m_r2*this.xyz_z),this.rgb_g=e.fromLinear(e.m_g0*this.xyz_x+e.m_g1*this.xyz_y+e.m_g2*this.xyz_z),this.rgb_b=e.fromLinear(e.m_b0*this.xyz_x+e.m_b1*this.xyz_y+e.m_b2*this.xyz_z)}rgbToXyz(){let t=e.toLinear(this.rgb_r),n=e.toLinear(this.rgb_g),r=e.toLinear(this.rgb_b);this.xyz_x=.41239079926595*t+.35758433938387*n+.18048078840183*r,this.xyz_y=.21263900587151*t+.71516867876775*n+.072192315360733*r,this.xyz_z=.019330818715591*t+.11919477979462*n+.95053215224966*r}xyzToLuv(){let t=this.xyz_x+15*this.xyz_y+3*this.xyz_z,n=4*this.xyz_x,r=9*this.xyz_y;t===0?(n=NaN,r=NaN):(n/=t,r/=t),this.luv_l=e.yToL(this.xyz_y),this.luv_l===0?(this.luv_u=0,this.luv_v=0):(this.luv_u=13*this.luv_l*(n-e.refU),this.luv_v=13*this.luv_l*(r-e.refV))}luvToXyz(){if(this.luv_l===0){this.xyz_x=0,this.xyz_y=0,this.xyz_z=0;return}let t=this.luv_u/(13*this.luv_l)+e.refU,n=this.luv_v/(13*this.luv_l)+e.refV;this.xyz_y=e.lToY(this.luv_l),this.xyz_x=0-9*this.xyz_y*t/((t-4)*n-t*n),this.xyz_z=(9*this.xyz_y-15*n*this.xyz_y-n*this.xyz_x)/(3*n)}luvToLch(){this.lch_l=this.luv_l,this.lch_c=Math.sqrt(this.luv_u*this.luv_u+this.luv_v*this.luv_v),this.lch_c<1e-8?this.lch_h=0:(this.lch_h=Math.atan2(this.luv_v,this.luv_u)*180/Math.PI,this.lch_h<0&&(this.lch_h=360+this.lch_h))}lchToLuv(){let e=this.lch_h/180*Math.PI;this.luv_l=this.lch_l,this.luv_u=Math.cos(e)*this.lch_c,this.luv_v=Math.sin(e)*this.lch_c}calculateBoundingLines(t){let n=(t+16)**3/1560896,r=n>e.epsilon?n:t/e.kappa,i=r*(284517*e.m_r0-94839*e.m_r2),a=r*(838422*e.m_r2+769860*e.m_r1+731718*e.m_r0),o=r*(632260*e.m_r2-126452*e.m_r1),s=r*(284517*e.m_g0-94839*e.m_g2),c=r*(838422*e.m_g2+769860*e.m_g1+731718*e.m_g0),l=r*(632260*e.m_g2-126452*e.m_g1),u=r*(284517*e.m_b0-94839*e.m_b2),d=r*(838422*e.m_b2+769860*e.m_b1+731718*e.m_b0),f=r*(632260*e.m_b2-126452*e.m_b1);this.r0s=i/o,this.r0i=a*t/o,this.r1s=i/(o+126452),this.r1i=(a-769860)*t/(o+126452),this.g0s=s/l,this.g0i=c*t/l,this.g1s=s/(l+126452),this.g1i=(c-769860)*t/(l+126452),this.b0s=u/f,this.b0i=d*t/f,this.b1s=u/(f+126452),this.b1i=(d-769860)*t/(f+126452)}calcMaxChromaHpluv(){let t=e.distanceFromOrigin(this.r0s,this.r0i),n=e.distanceFromOrigin(this.r1s,this.r1i),r=e.distanceFromOrigin(this.g0s,this.g0i),i=e.distanceFromOrigin(this.g1s,this.g1i),a=e.distanceFromOrigin(this.b0s,this.b0i),o=e.distanceFromOrigin(this.b1s,this.b1i);return e.min6(t,n,r,i,a,o)}calcMaxChromaHsluv(t){let n=t/360*Math.PI*2,r=e.distanceFromOriginAngle(this.r0s,this.r0i,n),i=e.distanceFromOriginAngle(this.r1s,this.r1i,n),a=e.distanceFromOriginAngle(this.g0s,this.g0i,n),o=e.distanceFromOriginAngle(this.g1s,this.g1i,n),s=e.distanceFromOriginAngle(this.b0s,this.b0i,n),c=e.distanceFromOriginAngle(this.b1s,this.b1i,n);return e.min6(r,i,a,o,s,c)}hsluvToLch(){this.hsluv_l>99.9999999?(this.lch_l=100,this.lch_c=0):this.hsluv_l<1e-8?(this.lch_l=0,this.lch_c=0):(this.lch_l=this.hsluv_l,this.calculateBoundingLines(this.hsluv_l),this.lch_c=this.calcMaxChromaHsluv(this.hsluv_h)/100*this.hsluv_s),this.lch_h=this.hsluv_h}lchToHsluv(){if(this.lch_l>99.9999999)this.hsluv_s=0,this.hsluv_l=100;else if(this.lch_l<1e-8)this.hsluv_s=0,this.hsluv_l=0;else{this.calculateBoundingLines(this.lch_l);let e=this.calcMaxChromaHsluv(this.lch_h);this.hsluv_s=this.lch_c/e*100,this.hsluv_l=this.lch_l}this.hsluv_h=this.lch_h}hpluvToLch(){this.hpluv_l>99.9999999?(this.lch_l=100,this.lch_c=0):this.hpluv_l<1e-8?(this.lch_l=0,this.lch_c=0):(this.lch_l=this.hpluv_l,this.calculateBoundingLines(this.hpluv_l),this.lch_c=this.calcMaxChromaHpluv()/100*this.hpluv_p),this.lch_h=this.hpluv_h}lchToHpluv(){if(this.lch_l>99.9999999)this.hpluv_p=0,this.hpluv_l=100;else if(this.lch_l<1e-8)this.hpluv_p=0,this.hpluv_l=0;else{this.calculateBoundingLines(this.lch_l);let e=this.calcMaxChromaHpluv();this.hpluv_p=this.lch_c/e*100,this.hpluv_l=this.lch_l}this.hpluv_h=this.lch_h}hsluvToRgb(){this.hsluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hpluvToRgb(){this.hpluvToLch(),this.lchToLuv(),this.luvToXyz(),this.xyzToRgb()}hsluvToHex(){this.hsluvToRgb(),this.rgbToHex()}hpluvToHex(){this.hpluvToRgb(),this.rgbToHex()}rgbToHsluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHsluv()}rgbToHpluv(){this.rgbToXyz(),this.xyzToLuv(),this.luvToLch(),this.lchToHpluv(),this.lchToHpluv()}hexToHsluv(){this.hexToRgb(),this.rgbToHsluv()}hexToHpluv(){this.hexToRgb(),this.rgbToHpluv()}};q.hexChars=`0123456789abcdef`,q.refY=1,q.refU=.19783000664283,q.refV=.46831999493879,q.kappa=903.2962962,q.epsilon=.0088564516,q.m_r0=3.240969941904521,q.m_r1=-1.537383177570093,q.m_r2=-.498610760293,q.m_g0=-.96924363628087,q.m_g1=1.87596750150772,q.m_g2=.041555057407175,q.m_b0=.055630079696993,q.m_b1=-.20397695888897,q.m_b2=1.056971514242878;var Bn=s(((e,t)=>{function n(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.exports=n})),Vn=s(((e,t)=>{var n=Bn(),r,i;function a(){for(var e in i=[`toString`,`toLocaleString`,`valueOf`,`hasOwnProperty`,`isPrototypeOf`,`propertyIsEnumerable`,`constructor`],r=!0,{toString:null})r=!1}function o(e,t,o){var c,l=0;for(c in r??a(),e)if(s(t,e,c,o)===!1)break;if(r)for(var u=e.constructor,d=!!u&&e===u.prototype;(c=i[l++])&&!((c!==`constructor`||!d&&n(e,c))&&e[c]!==Object.prototype[c]&&s(t,e,c,o)===!1););}function s(e,t,n,r){return e.call(r,t[n],n,t)}t.exports=o})),Hn=s(((e,t)=>{var n=Vn();function r(e){var t=[];return n(e,function(e,n){typeof e==`function`&&t.push(n)}),t.sort()}t.exports=r})),Un=s(((e,t)=>{function n(e,t,n){var r=e.length;t=t==null?0:t<0?Math.max(r+t,0):Math.min(t,r),n=n==null?r:n<0?Math.max(r+n,0):Math.min(n,r);for(var i=[];t{var n=Un();function r(e,t,r){var i=n(arguments,2);return function(){return e.apply(t,i.concat(n(arguments)))}}t.exports=r})),Gn=s(((e,t)=>{function n(e,t,n){if(e!=null)for(var r=-1,i=e.length;++r{var n=Hn(),r=Wn(),i=Gn(),a=Un();function o(e,t){i(arguments.length>1?a(arguments,1):n(e),function(t){e[t]=r(e[t],e)})}t.exports=o})),J=s(((e,t)=>{var n=Bn(),r=Vn();function i(e,t,i){r(e,function(r,a){if(n(e,a))return t.call(i,e[a],a,e)})}t.exports=i})),qn=s(((e,t)=>{function n(e){return e}t.exports=n})),Jn=s(((e,t)=>{function n(e){return function(t){return t[e]}}t.exports=n})),Yn=s(((e,t)=>{var n=/^\[object (.*)\]$/,r=Object.prototype.toString,i;function a(e){return e===null?`Null`:e===i?`Undefined`:n.exec(r.call(e))[1]}t.exports=a})),Xn=s(((e,t)=>{var n=Yn();function r(e,t){return n(e)===t}t.exports=r})),Zn=s(((e,t)=>{var n=Xn();t.exports=Array.isArray||function(e){return n(e,`Array`)}})),Qn=s(((e,t)=>{var n=J(),r=Zn();function i(e,t){for(var n=-1,r=e.length;++n{var n=qn(),r=Jn(),i=Qn();function a(e,t){if(e==null)return n;switch(typeof e){case`function`:return t===void 0?e:function(n,r,i){return e.call(t,n,r,i)};case`object`:return function(t){return i(t,e)};case`string`:case`number`:return r(e)}}t.exports=a})),$n=s(((e,t)=>{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a=!1;return n(e,function(n,r){if(t(n,r,e))return a=!0,!1}),a}t.exports=i})),er=s(((e,t)=>{var n=$n();function r(e,t){return n(e,function(e){return e===t})}t.exports=r})),tr=s(((e,t)=>{function n(e){return!!e&&typeof e==`object`&&e.constructor===Object}t.exports=n})),nr=s(((e,t)=>{var n=J(),r=tr();function i(e,t){for(var a=0,o=arguments.length,s;++a{var n=J(),r=tr();function i(e,t){for(var r=0,i=arguments.length,o;++r{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a=!0;return n(e,function(n,r){if(!t(n,r,e))return a=!1,!1}),a}t.exports=i})),ar=s(((e,t)=>{var n=Xn();function r(e){return n(e,`Object`)}t.exports=r})),or=s(((e,t)=>{function n(e,t){return e===t?e!==0||1/e==1/t:e!==e&&t!==t}t.exports=n})),sr=s(((e,t)=>{var n=Bn(),r=ir(),i=ar(),a=or();function o(e){return function(t,r){return n(this,r)&&e(t,this[r])}}function s(e,t){return n(this,t)}function c(e,t,n){return n||=a,!i(e)||!i(t)?n(e,t):r(e,o(n),t)&&r(t,s,e)}t.exports=c})),cr=s(((e,t)=>{var n=Gn(),r=Un(),i=J();function a(e,t){return n(r(arguments,1),function(t){i(t,function(t,n){e[n]??(e[n]=t)})}),e}t.exports=a})),lr=s(((e,t)=>{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a={};return n(e,function(e,n,r){t(e,n,r)&&(a[n]=e)}),a}t.exports=i})),ur=s(((e,t)=>{var n=$n(),r=Y();function i(e,t,i){t=r(t,i);var a;return n(e,function(e,n,r){if(t(e,n,r))return a=e,!0}),a}t.exports=i})),dr=s(((e,t)=>{var n=J(),r=tr();function i(e,t,a,o){return n(e,function(e,n){var s=a?a+`.`+n:n;o!==0&&r(e)?i(e,t,s,o-1):t[s]=e}),t}function a(e,t){return e==null?{}:(t??=-1,i(e,{},``,t))}t.exports=a})),fr=s(((e,t)=>{function n(e){switch(typeof e){case`string`:case`number`:case`boolean`:return!0}return e==null}t.exports=n})),pr=s(((e,t)=>{fr();function n(e,t){for(var n=t.split(`.`),r=n.pop();t=n.shift();)if(e=e[t],e==null)return;return e[r]}t.exports=n})),mr=s(((e,t)=>{var n=pr(),r;function i(e,t){return n(e,t)!==r}t.exports=i})),hr=s(((e,t)=>{var n=J();t.exports=Object.keys||function(e){var t=[];return n(e,function(e,n){t.push(n)}),t}})),gr=s(((e,t)=>{var n=J(),r=Y();function i(e,t,i){t=r(t,i);var a={};return n(e,function(e,n,r){a[n]=t(e,n,r)}),a}t.exports=i})),_r=s(((e,t)=>{var n=J();function r(e,t){var r=!0;return n(t,function(t,n){if(e[n]!==t)return r=!1}),r}t.exports=r})),vr=s(((e,t)=>{var n=Y();function r(e,t,r){if(e==null||!e.length)return 1/0;if(e.length&&!t)return Math.max.apply(Math,e);t=n(t,r);for(var i,a=-1/0,o,s,c=-1,l=e.length;++ca&&(a=s,i=o);return i}t.exports=r})),yr=s(((e,t)=>{var n=J();function r(e){var t=[];return n(e,function(e,n){t.push(e)}),t}t.exports=r})),br=s(((e,t)=>{var n=vr(),r=yr();function i(e,t){return n(r(e),t)}t.exports=i})),xr=s(((e,t)=>{var n=J();function r(e,t){for(var r=0,a=arguments.length,o;++r{var n=Yn(),r=tr(),i=xr();function a(e){switch(n(e)){case`Object`:return o(e);case`Array`:return l(e);case`RegExp`:return s(e);case`Date`:return c(e);default:return e}}function o(e){return r(e)?i({},e):e}function s(e){var t=``;return t+=e.multiline?`m`:``,t+=e.global?`g`:``,t+=e.ignoreCase?`i`:``,new RegExp(e.source,t)}function c(e){return new Date(+e)}function l(e){return e.slice()}t.exports=a})),Cr=s(((e,t)=>{var n=Sr(),r=J(),i=Yn(),a=tr();function o(e,t){switch(i(e)){case`Object`:return s(e,t);case`Array`:return c(e,t);default:return n(e)}}function s(e,t){if(a(e)){var n={};return r(e,function(e,n){this[n]=o(e,t)},n),n}else if(t)return t(e);else return e}function c(e,t){for(var n=[],r=-1,i=e.length;++r{var n=Bn(),r=Cr(),i=ar();function a(){for(var e=1,t,o,s,c=r(arguments[0]);s=arguments[e++];)for(t in s)n(s,t)&&(o=s[t],i(o)&&i(c[t])?c[t]=a(c[t],o):c[t]=r(o));return c}t.exports=a})),Tr=s(((e,t)=>{var n=Y();function r(e,t,r){if(e==null||!e.length)return-1/0;if(e.length&&!t)return Math.min.apply(Math,e);t=n(t,r);for(var i,a=1/0,o,s,c=-1,l=e.length;++c{var n=Tr(),r=yr();function i(e,t){return n(r(e),t)}t.exports=i})),Dr=s(((e,t)=>{var n=Gn();function r(e,t){return t&&n(t.split(`.`),function(t){e[t]||(e[t]={}),e=e[t]}),e}t.exports=r})),Or=s(((e,t)=>{function n(e,t,n){if(n||=0,e==null)return-1;for(var r=e.length,i=n<0?r+n:n;i{var n=Or();function r(e,t){return n(e,t)!==-1}t.exports=r})),Ar=s(((e,t)=>{var n=Un(),r=kr();function i(e,t){var i=typeof arguments[1]==`string`?n(arguments,1):arguments[1],a={};for(var o in e)e.hasOwnProperty(o)&&!r(i,o)&&(a[o]=e[o]);return a}t.exports=i})),jr=s(((e,t)=>{var n=Un();function r(e,t){for(var r=typeof arguments[1]==`string`?n(arguments,1):arguments[1],i={},a=0,o;o=r[a++];)i[o]=e[o];return i}t.exports=r})),Mr=s(((e,t)=>{var n=gr(),r=Jn();function i(e,t){return n(e,r(t))}t.exports=i})),Nr=s(((e,t)=>{var n=J();function r(e){var t=0;return n(e,function(){t++}),t}t.exports=r})),Pr=s(((e,t)=>{var n=J(),r=Nr();function i(e,t,i,a){var o=arguments.length>2;if(!r(e)&&!o)throw Error(`reduce of empty object with no initial value`);return n(e,function(e,n,r){o?i=t.call(a,i,e,n,r):(i=e,o=!0)}),i}t.exports=i})),Fr=s(((e,t)=>{var n=lr(),r=Y();function i(e,t,i){return t=r(t,i),n(e,function(e,n,r){return!t(e,n,r)},i)}t.exports=i})),Ir=s(((e,t)=>{var n=Xn();function r(e){return n(e,`Function`)}t.exports=r})),Lr=s(((e,t)=>{var n=Ir();function r(e,t){var r=e[t];if(r!==void 0)return n(r)?r.call(e):r}t.exports=r})),Rr=s(((e,t)=>{var n=Dr();function r(e,t,r){var i=/^(.+)\.(.+)$/.exec(t);i?n(e,i[1])[i[2]]=r:e[t]=r}t.exports=r})),zr=s(((e,t)=>{var n=mr();function r(e,t){if(n(e,t)){for(var r=t.split(`.`),i=r.pop();t=r.shift();)e=e[t];return delete e[i]}else return!0}t.exports=r})),Br=s(((e,t)=>{t.exports={bindAll:Kn(),contains:er(),deepFillIn:nr(),deepMatches:Qn(),deepMixIn:rr(),equals:sr(),every:ir(),fillIn:cr(),filter:lr(),find:ur(),flatten:dr(),forIn:Vn(),forOwn:J(),functions:Hn(),get:pr(),has:mr(),hasOwn:Bn(),keys:hr(),map:gr(),matches:_r(),max:br(),merge:wr(),min:Er(),mixIn:xr(),namespace:Dr(),omit:Ar(),pick:jr(),pluck:Mr(),reduce:Pr(),reject:Fr(),result:Lr(),set:Rr(),size:Nr(),some:$n(),unset:zr(),values:yr()}})),Vr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.default=(0,Br().map)({A:{x:.44758,y:.40745},C:{x:.31006,y:.31616},D50:{x:.34567,y:.35851},D65:{x:.31272,y:.32903},D55:{x:.33243,y:.34744},D75:{x:.29903,y:.31488}},function(e){return[100*(e.x/e.y),100,100*(1-e.x-e.y)/e.y]}),t.exports=e.default})),Hr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0});var n=Math,r=n.pow,i=n.sign,a=n.abs,o={decode:function(e){return e<=.04045?e/12.92:r((e+.055)/1.055,2.4)},encode:function(e){return e<=.0031308?12.92*e:1.055*r(e,1/2.4)-.055}},s={encode:function(e){return e<.001953125?16*e:r(e,1/1.8)},decode:function(e){return e<16*.001953125?e/16:r(e,1.8)}};function c(e){return{decode:function(t){return i(t)*r(a(t),e)},encode:function(t){return i(t)*r(a(t),1/e)}}}e.default={sRGB:{r:{x:.64,y:.33},g:{x:.3,y:.6},b:{x:.15,y:.06},gamma:o},"Adobe RGB":{r:{x:.64,y:.33},g:{x:.21,y:.71},b:{x:.15,y:.06},gamma:c(2.2)},"Wide Gamut RGB":{r:{x:.7347,y:.2653},g:{x:.1152,y:.8264},b:{x:.1566,y:.0177},gamma:c(563/256)},"ProPhoto RGB":{r:{x:.7347,y:.2653},g:{x:.1596,y:.8404},b:{x:.0366,y:1e-4},gamma:s}},t.exports=e.default})),Ur=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0});function t(e){return[[e[0][0],e[1][0],e[2][0]],[e[0][1],e[1][1],e[2][1]],[e[0][2],e[1][2],e[2][2]]]}function n(e){return e[0][0]*(e[2][2]*e[1][1]-e[2][1]*e[1][2])+e[1][0]*(e[2][1]*e[0][2]-e[2][2]*e[0][1])+e[2][0]*(e[1][2]*e[0][1]-e[1][1]*e[0][2])}function r(e){var t=1/n(e);return[[(e[2][2]*e[1][1]-e[2][1]*e[1][2])*t,(e[2][1]*e[0][2]-e[2][2]*e[0][1])*t,(e[1][2]*e[0][1]-e[1][1]*e[0][2])*t],[(e[2][0]*e[1][2]-e[2][2]*e[1][0])*t,(e[2][2]*e[0][0]-e[2][0]*e[0][2])*t,(e[1][0]*e[0][2]-e[1][2]*e[0][0])*t],[(e[2][1]*e[1][0]-e[2][0]*e[1][1])*t,(e[2][0]*e[0][1]-e[2][1]*e[0][0])*t,(e[1][1]*e[0][0]-e[1][0]*e[0][1])*t]]}function i(e,t){return[e[0][0]*t[0]+e[0][1]*t[1]+e[0][2]*t[2],e[1][0]*t[0]+e[1][1]*t[1]+e[1][2]*t[2],e[2][0]*t[0]+e[2][1]*t[1]+e[2][2]*t[2]]}function a(e,t){return[[e[0][0]*t[0],e[0][1]*t[1],e[0][2]*t[2]],[e[1][0]*t[0],e[1][1]*t[1],e[1][2]*t[2]],[e[2][0]*t[0],e[2][1]*t[1],e[2][2]*t[2]]]}function o(e,t){return[[e[0][0]*t[0][0]+e[0][1]*t[1][0]+e[0][2]*t[2][0],e[0][0]*t[0][1]+e[0][1]*t[1][1]+e[0][2]*t[2][1],e[0][0]*t[0][2]+e[0][1]*t[1][2]+e[0][2]*t[2][2]],[e[1][0]*t[0][0]+e[1][1]*t[1][0]+e[1][2]*t[2][0],e[1][0]*t[0][1]+e[1][1]*t[1][1]+e[1][2]*t[2][1],e[1][0]*t[0][2]+e[1][1]*t[1][2]+e[1][2]*t[2][2]],[e[2][0]*t[0][0]+e[2][1]*t[1][0]+e[2][2]*t[2][0],e[2][0]*t[0][1]+e[2][1]*t[1][1]+e[2][2]*t[2][1],e[2][0]*t[0][2]+e[2][1]*t[1][2]+e[2][2]*t[2][2]]]}e.transpose=t,e.determinant=n,e.inverse=r,e.multiply=i,e.scalar=a,e.product=o})),Wr=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0});var t=Math.PI;function n(e){for(var n=e*180/t;n<0;)n+=360;for(;n>360;)n-=360;return n}function r(e){for(var n=t*e/180;n<0;)n+=2*t;for(;n>2*t;)n-=2*t;return n}e.fromRadian=n,e.toRadian=r})),Gr=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0});var t=Math.round;function n(e){return e[0]==`#`&&(e=e.slice(1)),e.length<6&&(e=e.split(``).map(function(e){return e+e}).join(``)),e.match(/../g).map(function(e){return parseInt(e,16)/255})}function r(e){return`#`+e.map(function(e){return e=t(255*e).toString(16),e.length<2&&(e=`0`+e),e}).join(``)}e.fromHex=n,e.toHex=r})),Kr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0});var n=o(Ur()),r=a(Vr()),i=a(Hr());function a(e){return e&&e.__esModule?e:{default:e}}function o(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function s(){var e=arguments.length<=0||arguments[0]===void 0?i.default.sRGB:arguments[0],t=arguments.length<=1||arguments[1]===void 0?r.default.D65:arguments[1],a=[e.r,e.g,e.b],o=n.transpose(a.map(function(e){return[e.x/e.y,1,(1-e.x-e.y)/e.y]})),s=e.gamma,c=n.multiply(n.inverse(o),t),l=n.scalar(o,c),u=n.inverse(l);return{fromRgb:function(e){return n.multiply(l,e.map(s.decode))},toRgb:function(e){return n.multiply(u,e).map(s.encode)}}}e.default=s,t.exports=e.default})),qr=s(((e,t)=>{t.exports={illuminant:Vr(),workspace:Hr(),matrix:Ur(),degree:Wr(),rgb:Gr(),xyz:Kr()}})),Jr=s((e=>{Object.defineProperty(e,`__esModule`,{value:!0}),e.cfs=e.distance=e.lerp=e.corLerp=void 0;var t=Br();function n(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);ti/2&&(e>t?t+=i:e+=i),((1-n)*e+n*t)%(i||1/0)}function u(e,t,n){var r={};for(var i in e)r[i]=l(e[i],t[i],n,i);return r}function d(e,t){var n=0;for(var r in e)n+=o(e[r]-t[r],2);return s(n)}function f(e){return t.merge.apply(void 0,r(e.split(``).map(function(e){return n({},e,!0)})))}e.corLerp=l,e.lerp=u,e.distance=d,e.cfs=f})),Yr=s(((e,t)=>{var n=function(){function e(e,t){var n=[],r=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(r=(s=o.next()).done)&&(n.push(s.value),!(t&&n.length===t));r=!0);}catch(e){i=!0,a=e}finally{try{!r&&o.return&&o.return()}finally{if(i)throw a}}return n}return function(t,n){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,n);throw TypeError(`Invalid attempt to destructure non-iterable instance`)}}();Object.defineProperty(e,`__esModule`,{value:!0});var r=qr(),i=Jr();function a(e,t){var a=arguments.length<=2||arguments[2]===void 0?1e-6:arguments[2],o=-a,s=1+a,c=Math,l=c.min,u=c.max,d=n([`000`,`fff`].map(function(n){return t.fromXyz(e.fromRgb(r.rgb.fromHex(n)))}),2),f=d[0],p=d[1];function m(n){var r=e.toRgb(t.toXyz(n));return[r.map(function(e){return e>=o&&e<=s}).reduce(function(e,t){return e&&t},!0),r]}function h(e,t){for(var r=arguments.length<=2||arguments[2]===void 0?.001:arguments[2];(0,i.distance)(e,t)>r;){var a=(0,i.lerp)(e,t,.5);n(m(a),1)[0]?e=a:t=a}return e}function g(e){return(0,i.lerp)(f,p,e)}function _(e){return e.map(function(e){return u(o,l(s,e))})}return{contains:m,limit:h,spine:g,crop:_}}e.default=a,t.exports=e.default})),Xr=s((e=>{var t=function(){function e(e,t){var n=[],r=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(r=(s=o.next()).done)&&(n.push(s.value),!(t&&n.length===t));r=!0);}catch(e){i=!0,a=e}finally{try{!r&&o.return&&o.return()}finally{if(i)throw a}}return n}return function(t,n){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,n);throw TypeError(`Invalid attempt to destructure non-iterable instance`)}}();Object.defineProperty(e,`__esModule`,{value:!0}),e.toNotation=e.fromNotation=e.toHue=e.fromHue=void 0;var n=Jr(),r=Math.floor,i=[{s:`R`,h:20.14,e:.8,H:0},{s:`Y`,h:90,e:.7,H:100},{s:`G`,h:164.25,e:1,H:200},{s:`B`,h:237.53,e:1.2,H:300},{s:`R`,h:380.14,e:.8,H:400}],a=i.map(function(e){return e.s}).slice(0,-1).join(``);function o(e){e50){var o=[n,t];t=o[0],n=o[1],i=100-i}return i<1?a[t]:a[t]+i.toFixed()+a[n]}e.fromHue=o,e.toHue=s,e.fromNotation=l,e.toNotation=u})),Zr=s(((e,t)=>{var n=function(){function e(e,t){var n=[],r=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(r=(s=o.next()).done)&&(n.push(s.value),!(t&&n.length===t));r=!0);}catch(e){i=!0,a=e}finally{try{!r&&o.return&&o.return()}finally{if(i)throw a}}return n}return function(t,n){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,n);throw TypeError(`Invalid attempt to destructure non-iterable instance`)}}();Object.defineProperty(e,`__esModule`,{value:!0});var r=qr(),i=s(Xr()),a=Jr(),o=Br();function s(e){if(e&&e.__esModule)return e;var t={};if(e!=null)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}var c=Math,l=c.pow,u=c.sqrt,d=c.exp,f=c.abs,p=c.sign,m=Math,h=m.sin,g=m.cos,_=m.atan2,v={average:{F:1,c:.69,N_c:1},dim:{F:.9,c:.59,N_c:.9},dark:{F:.8,c:.535,N_c:.8}},y=[[.7328,.4296,-.1624],[-.7036,1.6975,.0061],[.003,.0136,.9834]],b=[[.38971,.68898,-.07868],[-.22981,1.1834,.04641],[0,0,1]],x=y,S=r.matrix.inverse(y),C=r.matrix.product(b,r.matrix.inverse(y)),w=r.matrix.product(y,r.matrix.inverse(b)),T={whitePoint:r.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:`average`,discounting:!1},ee=(0,a.cfs)(`QJMCshH`),E=(0,a.cfs)(`JCh`);function D(){var e=arguments.length<=0||arguments[0]===void 0?{}:arguments[0],t=arguments.length<=1||arguments[1]===void 0?ee:arguments[1];e=(0,o.merge)(T,e);var a=e.whitePoint,s=e.adaptingLuminance,c=e.backgroundLuminance,m=v[e.surroundType],b=m.F,D=m.c,O=m.N_c,te=a[1],k=1/(5*s+1),A=.2*l(k,4)*5*s+.1*l(1-l(k,4),2)*l(5*s,1/3),ne=c/te,re=.725*l(1/ne,.2),ie=re,ae=1.48+u(ne),oe=e.discounting?1:b*(1-1/3.6*d(-(s+42)/92)),j=n(r.matrix.multiply(y,a).map(function(e){return oe*te/e+1-oe}),3),se=j[0],M=j[1],ce=j[2],N=pe(de(le(a)));function le(e){var t=n(r.matrix.multiply(x,e),3),i=t[0],a=t[1],o=t[2];return[se*i,M*a,ce*o]}function ue(e){var t=n(e,3),i=t[0],a=t[1],o=t[2];return r.matrix.multiply(S,[i/se,a/M,o/ce])}function de(e){return r.matrix.multiply(C,e).map(function(e){var t=l(A*f(e)/100,.42);return p(e)*400*t/(27.13+t)+.1})}function fe(e){return r.matrix.multiply(w,e.map(function(e){var t=e-.1;return p(t)*100/A*l(27.13*f(t)/(400-f(t)),1/.42)}))}function pe(e){var t=n(e,3),r=t[0],i=t[1],a=t[2];return(r*2+i+a/20-.305)*re}function me(e){return 4/D*u(e/100)*(N+4)*l(A,.25)}function he(e){return 6.25*l(D*e/((N+4)*l(A,.25)),2)}function ge(e){return e*l(A,.25)}function _e(e,t){return l(e/100,2)*t/l(A,.25)}function ve(e){return e/l(A,.25)}function ye(e,t){return 100*u(e/t)}function be(e,t){var n=t.Q,r=t.J,a=t.M,o=t.C,s=t.s,c=t.h,l=t.H,u={};return e.J&&(u.J=isNaN(r)?he(n):r),e.C&&(isNaN(o)?isNaN(a)?(n=isNaN(n)?me(r):n,u.C=_e(s,n)):u.C=ve(a):u.C=t.C),e.h&&(u.h=isNaN(c)?i.toHue(l):c),e.Q&&(u.Q=isNaN(n)?me(r):n),e.M&&(u.M=isNaN(a)?ge(o):a),e.s&&(isNaN(s)?(n=isNaN(n)?me(r):n,a=isNaN(a)?ge(o):a,u.s=ye(a,n)):u.s=s),e.H&&(u.H=isNaN(l)?i.fromHue(c):l),u}function P(e){var i=de(le(e)),a=n(i,3),o=a[0],s=a[1],c=a[2],d=o-s*12/11+c/11,f=(o+s-2*c)/9,p=_(f,d),m=r.degree.fromRadian(p),h=1/4*(g(p+2)+3.8),v=100*l(pe(i)/N,D*ae);return be(t,{J:v,C:l(5e4/13*O*ie*h*u(d*d+f*f)/(o+s+21/20*c),.9)*u(v/100)*l(1.64-l(.29,ne),.73),h:m})}function F(e){var t=be(E,e),n=t.J,i=t.C,a=t.h,o=r.degree.toRadian(a),s=l(i/(u(n/100)*l(1.64-l(.29,ne),.73)),10/9),c=1/4*(g(o+2)+3.8),d=N*l(n/100,1/D/ae),p=5e4/13*O*ie*c/s,m=d/re+.305,_=m*61/20*460/1403,v=61/20*220/1403,y=21/20*6300/1403-27/1403,b=h(o),x=g(o),S,C;return s===0||isNaN(s)?S=C=0:f(b)>=f(x)?(C=_/(p/b+v*x/b+y),S=C*x/b):(S=_/(p/x+v+y*b/x),C=S*b/x),ue(fe([20/61*m+451/1403*S+288/1403*C,20/61*m-891/1403*S-261/1403*C,20/61*m-220/1403*S-6300/1403*C]))}return{fromXyz:P,toXyz:F,fillOut:be}}e.default=D,t.exports=e.default})),Qr=s(((e,t)=>{Object.defineProperty(e,`__esModule`,{value:!0});var n=qr(),r=Math,i=r.sqrt,a=r.pow,o=r.exp,s=r.log,c=r.cos,l=r.sin,u=r.atan2,d={LCD:{K_L:.77,c_1:.007,c_2:.0053},SCD:{K_L:1.24,c_1:.007,c_2:.0363},UCS:{K_L:1,c_1:.007,c_2:.0228}};function f(){var e=d[arguments.length<=0||arguments[0]===void 0?`UCS`:arguments[0]],t=e.K_L,r=e.c_1,f=e.c_2;function p(e){var t=e.J,i=e.M,a=e.h,o=n.degree.toRadian(a),u=(1+100*r)*t/(1+r*t),d=1/f*s(1+f*i);return{J_p:u,a_p:d*c(o),b_p:d*l(o)}}function m(e){var t=e.J_p,s=e.a_p,c=e.b_p,l=-t/(r*t-100*r-1),d=(o(f*i(a(s,2)+a(c,2)))-1)/f,p=u(c,s);return{J:l,M:d,h:n.degree.fromRadian(p)}}function h(e,n){return i(a((e.J_p-n.J_p)/t,2)+a(e.a_p-n.a_p,2)+a(e.b_p-n.b_p,2))}return{fromCam:p,toCam:m,distance:h}}e.default=f,t.exports=e.default})),$r=s(((e,t)=>{var n=Jr(),r=Yr(),i=Zr(),a=Qr(),o=Xr();t.exports={gamut:r,cfs:n.cfs,lerp:n.lerp,cam:i,ucs:a,hq:o}})),ei=l(qr(),1),ti=l($r(),1);function ni(e){let t=new q;return t.rgb_r=e[0],t.rgb_g=e[1],t.rgb_b=e[2],t.rgbToHsluv(),[t.hsluv_h,t.hsluv_s,t.hsluv_l]}function ri(e){let t=new q;return t.hsluv_h=e[0],t.hsluv_s=e[1],t.hsluv_l=e[2],t.hsluvToRgb(),[t.rgb_r,t.rgb_g,t.rgb_b]}var ii=ti.default.cam({whitePoint:ei.default.illuminant.D65,adaptingLuminance:40,backgroundLuminance:20,surroundType:`average`,discounting:!1},ti.default.cfs(`JCh`)),ai=ei.default.xyz(ei.default.workspace.sRGB,ei.default.illuminant.D65),oi=e=>ai.toRgb(ii.toXyz({J:e[0],C:e[1],h:e[2]})),si=e=>{let t=ii.fromXyz(ai.fromRgb(e));return[t.J,t.C,t.h]},[ci,li]=(()=>{let e={k_l:1,c1:.007,c2:.0228},t=Math.PI,n=64/t/5,r=1/(5*n+1),i=.2*r**4*(5*n)+.1*(1-r**4)**2*(5*n)**(1/3);return[n=>{let[r,a,o]=n,s=a*i**.25,c=(1+100*e.c1)*r/(1+e.c1*r);c/=e.k_l;let l=1/e.c2*Math.log(1+e.c2*s),u=l*Math.cos(t/180*o),d=l*Math.sin(t/180*o);return[c,u,d]},n=>{let[r,a,o]=n,s=Math.sqrt(a*a+o*o),c=(Math.exp(s*e.c2)-1)/e.c2,l=(180/t*Math.atan2(o,a)+360)%360,u=c/i**.25;return[r/(1+e.c1*(100-r)),u,l]}]})(),ui=e=>oi(li(e)),di=e=>ci(si(e)),fi=console;fi.color=(e,t=``)=>{let n=K(e).luminance();fi.log(`%c${e} ${t}`,`background-color: ${e};padding: 5px; border-radius: 5px; color: ${n>.5?`#000`:`#fff`}`)},fi.ramp=(e,t=1)=>{fi.log(`%c `,`font-size: 1px;line-height: 16px;background: ${K.getCSSGradient(e,t)};padding: 0 0 0 200px; border-radius: 2px;`)};var pi=(e,t,n,r,i,a,o=.1)=>{if(e===n||t===r)return!0;let s=(r-t)/(n-e),c=(a+i/s-t+s*e)/(s+1/s),l=a+i/s-c/s;return(i-c)**2+(a-l)**2{let i=(t[0]+n[0])/2,a=e(i);return pi(...t,...n,i,a,r)?null:[i,a]},hi=(e,t,n,r=.1)=>{let i=(n-t)/10,a=[];for(let r=t;rMath.round(e*10**t)/10**t,_i=(e,t=1,n=90,r=.005)=>{let i=hi(t=>e(t).gl()[0],0,t,r),a=hi(t=>e(t).gl()[1],0,t,r),o=hi(t=>e(t).gl()[2],0,t,r);return`linear-gradient(${n}deg, ${Array.from(new Set([...i.map(e=>gi(e[0])),...a.map(e=>gi(e[0])),...o.map(e=>gi(e[0]))].sort((e,t)=>e-t))).map(t=>`${e(t).hex()} ${gi(t*100)}%`).join()});`},vi=e=>{e.Color.prototype.jch=function(){return si(this._rgb.slice(0,3).map(e=>e/255))},e.jch=(...t)=>new e.Color(...oi(t).map(e=>Math.floor(e*255)),`rgb`),e.Color.prototype.jab=function(){return di(this._rgb.slice(0,3).map(e=>e/255))},e.jab=(...t)=>new e.Color(...ui(t).map(e=>Math.floor(e*255)),`rgb`),e.Color.prototype.hsluv=function(){return ni(this._rgb.slice(0,3).map(e=>e/255))},e.hsluv=(...t)=>new e.Color(...ri(t).map(e=>Math.floor(e*255)),`rgb`);let t=e.interpolate,n={jch:si,jab:di,hsluv:ni},r=(e,t,n)=>(Math.abs(e-t)>360/2&&(e>t?t+=360:e+=360),((1-n)*e+n*t)%360);e.interpolate=(i,a,o=.5,s=`lrgb`)=>{if(n[s]){typeof i!=`object`&&(i=new e.Color(i)),typeof a!=`object`&&(a=new e.Color(a));let t=n[s](i.gl()),c=n[s](a.gl()),l=Number.isNaN(i.hsl()[0]),u=Number.isNaN(a.hsl()[0]),d,f,p;switch(s){case`hsluv`:t[1]<1e-10&&(t[0]=c[0]),t[1]===0&&(t[1]=c[1]),c[1]<1e-10&&(c[0]=t[0]),c[1]===0&&(c[1]=t[1]),d=r(t[0],c[0],o),f=t[1]+(c[1]-t[1])*o,p=t[2]+(c[2]-t[2])*o;break;case`jch`:l&&(t[2]=c[2]),u&&(c[2]=t[2]),d=t[0]+(c[0]-t[0])*o,f=t[1]+(c[1]-t[1])*o,p=r(t[2],c[2],o);break;default:d=t[0]+(c[0]-t[0])*o,f=t[1]+(c[1]-t[1])*o,p=t[2]+(c[2]-t[2])*o}return e[s](d,f,p).alpha(i.alpha()+o*(a.alpha()-i.alpha()))}return t(i,a,o,s)},e.getCSSGradient=_i},X={mainTRC:2.4,get mainTRCencode(){return 1/this.mainTRC},sRco:.2126729,sGco:.7151522,sBco:.072175,normBG:.56,normTXT:.57,revTXT:.62,revBG:.65,blkThrs:.022,blkClmp:1.414,scaleBoW:1.14,scaleWoB:1.14,loBoWoffset:.027,loWoBoffset:.027,deltaYmin:5e-4,loClip:.1,mFactor:1.9468554433171,get mFactInv(){return 1/this.mFactor},mOffsetIn:.0387393816571401,mExpAdj:.283343396420869,get mExp(){return this.mExpAdj/this.blkClmp},mOffsetOut:.312865795870758};function yi(e,t,n=-1){let r=[0,1.1];if(isNaN(e)||isNaN(t)||Math.min(e,t)r[1])return 0;let i=0,a=0,o=`BoW`;return e=e>X.blkThrs?e:e+(X.blkThrs-e)**+X.blkClmp,t=t>X.blkThrs?t:t+(X.blkThrs-t)**+X.blkClmp,Math.abs(t-e)e?(i=(t**+X.normBG-e**+X.normTXT)*X.scaleBoW,a=i-X.loClip?0:i+X.loWoBoffset),n<0?a*100:n==0?Math.round(Math.abs(a)*100)+``+o+``:Number.isInteger(n)?(a*100).toFixed(n):0)}function bi(e=[0,0,0]){function t(e){return(e/255)**X.mainTRC}return X.sRco*t(e[0])+X.sGco*t(e[1])+X.sBco*t(e[2])}var xi=(e,t,n,r,i,a,o,s,c)=>{let l=1-c,u=l*l,d=u*l,f=c*c*c;return{x:d*e+u*3*c*n+l*3*c*c*i+f*o,y:d*t+u*3*c*r+l*3*c*c*a+f*s}},Si=(e,t)=>{let n=[],r={x:+e[0],y:+e[1]};for(let i=0,a=e.length;a-2*!t>i;i+=2){let o=[{x:+e[i-2],y:+e[i-1]},{x:+e[i],y:+e[i+1]},{x:+e[i+2],y:+e[i+3]},{x:+e[i+4],y:+e[i+5]}];t?i?a-4===i?o[3]={x:+e[0],y:+e[1]}:a-2===i&&(o[2]={x:+e[0],y:+e[1]},o[3]={x:+e[2],y:+e[3]}):o[0]={x:+e[a-2],y:+e[a-1]}:a-4===i?o[3]=o[2]:i||(o[0]={x:+e[i],y:+e[i+1]}),n.push([r.x,r.y,(-o[0].x+6*o[1].x+o[2].x)/6,(-o[0].y+6*o[1].y+o[2].y)/6,(o[1].x+6*o[2].x-o[3].x)/6,(o[1].y+6*o[2].y-o[3].y)/6,o[2].x,o[2].y]),r=o[2]}return n},Ci=(e,t,n,r,i,a,o,s)=>{let c=e,l=t,u=0;for(let d=1;d<5;d++){let{x:f,y:p}=xi(e,t,n,r,i,a,o,s,d/5);u+=Math.hypot(f-c,p-l),c=f,l=p}return u+=Math.hypot(o-c,s-l),u},wi=(e,t,n,r,i,a,o,s)=>{let c=Math.floor(Ci(e,t,n,r,i,a,o,s)*.75),l=[],u=0;for(let d=0;d<=c;d++){let f=xi(e,t,n,r,i,a,o,s,d/c),p=Math.round(f.x);if(l[p]=f.y,p-u>1){let e=l[u],t=l[p];for(let n=u+1;nl[Math.round(e)]||null},Ti={CAM02:`jab`,CAM02p:`jch`,HEX:`hex`,HSL:`hsl`,HSLuv:`hsluv`,HSV:`hsv`,LAB:`lab`,LCH:`lch`,RGB:`rgb`,OKLAB:`oklab`,OKLCH:`oklch`};function Z(e,t=0){let n=10**t;return Math.round(e*n)/n}function Ei(e,t){let n;return n=e>1?(e-1)*t+1:e<-1?(e+1)*t-1:1,Z(n,2)}function Di(e){return K(String(e)).jch()}function Oi(e){return K(String(e)).hsluv()}function ki(e,t,n){let r=[[],[],[]];if(e.forEach((e,n)=>r.forEach((r,i)=>r.push(t[n],e[i]))),n===`hcl`){let e=r[1];for(let t=1;t{let t=[];for(let n=1;n{e[t]=e[n]}),t.length=0;break}if(t.length){let n=K(`#ccc`).jch()[2];t.forEach(t=>{e[t]=n})}t.length=0;for(let n=e.length-1;n>0;n-=2)if(Number.isNaN(e[n]))t.push(n);else{t.forEach(t=>{e[t]=e[n]});break}for(let t=1;tSi(e).map(e=>wi(...e)));return e=>{let t=i.map(t=>{for(let n=0;nr*t**e+i}function ji({swatches:e,colorKeys:t,colorspace:n,colorSpace:r=n??`LAB`,shift:i=1,fullScale:a=!0,smooth:o=!1,distributeLightness:s=`linear`,sortColor:c=!0,asFun:l=!1}={}){n!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead.");let u=Ti[r];if(!u)throw Error(`Colorspace “${r}” not supported`);if(!t)throw Error(`Colorkeys missing: returned “${t}”`);let d;if(a)d=t.map(t=>e-e*(K(t).jch()[0]/100)).sort((e,t)=>e-t).concat(e),d.unshift(0);else{let n=t.map(e=>K(e).jch()[0]/100),r=Math.min(...n),i=Math.max(...n);d=n.map(t=>t===0||isNaN((t-r)/(i-r))?0:e-(t-r)/(i-r)*e).sort((e,t)=>e-t)}let f=Ai(i,[1,e],[1,e]);if(f=d.map(e=>Math.max(0,f(e))),d=f,s===`polynomial`){let t=e=>Math.sqrt(Math.sqrt((e**2.25+e**4)/2));d=f.map(t=>t/e).map(n=>t(n)*e)}let p=t.map((e,t)=>({colorKeys:Di(e),index:t})).sort((e,t)=>t.colorKeys[0]-e.colorKeys[0]).map(e=>t[e.index]),m=[],h;if(a){let e=u===`lch`?K.lch(...K(`#fff`).lch()):`#ffffff`,t=u===`lch`?K.lch(...K(`#000`).lch()):`#000000`;m=[e,...p,t]}else m=c?p:t;let g;if(o){let t=m;if(m=m.map(e=>K(String(e))[u]()),u===`hcl`&&m.forEach(e=>{e[1]=Number.isNaN(e[1])?0:e[1]}),u===`jch`)for(let e=0;eh(t))}else h=K.scale(m.map(e=>typeof e==`object`&&e.constructor===K.Color?e:String(e))).domain(d).mode(u);return l?h:(!o||o===!1?h.colors(e):g).filter(e=>e!=null)}function Mi(e,t){let n=[],r={};return Object.keys(e).forEach(n=>{r[e[n][t]]=e[n]}),Object.keys(r).forEach(e=>n.push(r[e])),n}function Ni(e){return Number.isNaN(e)?0:e}function Pi(e,t,n=!1){if(!e)throw Error(`Cannot convert color value of “${e}”`);if(!Ti[t])throw Error(`Cannot convert to colorspace “${t}”`);let r=Ti[t],i=K(String(e))[r]();if(t===`HSL`&&i.pop(),t===`HEX`){if(n){let t=K(String(e)).rgb();return{r:t[0],g:t[1],b:t[2]}}return i}let a={},o=i.map(Ni);o=o.map((e,t)=>{let i=Z(e),o=t;r===`hsluv`&&(o+=2);let s=r.charAt(o);return r===`jch`&&s===`c`&&(s=`C`),a[s===`j`?`J`:s]=i,r in{lab:1,lch:1,jab:1,jch:1}?n||(s===`l`||s===`j`)&&(i+=`%`):r!==`hsluv`&&(s===`s`||s===`l`||s===`v`)&&(a[s]=Z(e,2),n||(i=Z(e*100),i+=`%`)),i});let s=`${r}(${o.join(`, `)})`;return n?a:s}function Fi(e,t,n){let r=[e,t,n].map(e=>(e/=255,e<=.03928?e/12.92:((e+.055)/1.055)**2.4));return r[0]*.2126+r[1]*.7152+r[2]*.0722}function Ii(e,t,n,r=`wcag2`){if(n===void 0){let e=K.rgb(...t).hsluv()[2];n=Z(e/100,2)}if(r===`wcag2`){let r=Fi(e[0],e[1],e[2]),i=Fi(t[0],t[1],t[2]),a=(r+.05)/(i+.05),o=(i+.05)/(r+.05);return n<.5?a>=1?a:-o:a<1?o:a===1?a:-a}else if(r===`wcag3`)return n<.5?yi(bi(e),bi(t))*-1:yi(bi(e),bi(t));else throw Error(`Contrast calculation method ${r} unsupported; use 'wcag2' or 'wcag3'`)}function Li(e,t){if(!e)throw Error(`Array undefined`);if(!Array.isArray(e))throw Error(`Passed object is not an array`);let n=t===`wcag2`?0:1;return Math.min(...e.filter(e=>e>=n))}function Ri(e,t){if(!e)throw Error(`Ratios undefined`);e=e.sort((e,t)=>e-t);let n=Li(e,t),r=e.indexOf(n),i=[],a=e.slice(0,r),o=e.slice(r,e.length);for(let e=0;ee-t),i}var zi=(e,t,n,r,i)=>{let a=3e3,o=ji({swatches:a,colorKeys:e._modifiedKeys,colorspace:e._colorspace,shift:1,smooth:e._smooth,asFun:!0}),s={},c=e=>{if(s[e])return s[e];let r=Ii(K(o(e)).rgb(),t,n,i);return s[e]=r,r},l=e=>{let t=c(0).01&&o;)o--,n/=2,iu.push(o(l(+e)))),u},Q=class{constructor({name:e,colorKeys:t,colorspace:n,colorSpace:r=n??`RGB`,ratios:i,smooth:a=!1,output:o=`HEX`,saturation:s=100}){if(n!==void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),this._name=e,this._colorKeys=t,this._modifiedKeys=t,this._colorspace=r,this._ratios=i,this._smooth=a,this._output=o,this._saturation=s,!this._name)throw Error(`Color missing name`);if(!this._colorKeys)throw Error(`Color Keys are undefined`);if(!Ti[this._colorspace])throw Error(`Colorspace “${r}” not supported`);if(!Ti[this._output])throw Error(`Output “${this._output}” not supported`);for(let e=0;e{let n=K(`${t}`).oklch(),r=n[1]*(this._saturation/100),i=K.oklch(n[0],r,n[2]),a=K.rgb(i).hex();e.push(a)}),this._modifiedKeys=e,this._generateColorScale()}_generateColorScale(){this._colorScale=ji({swatches:3e3,colorKeys:this._modifiedKeys,colorSpace:this._colorspace,shift:1,smooth:this._smooth,asFun:!0})}},Bi=class extends Q{get backgroundColorScale(){return this._backgroundColorScale||this._generateColorScale(),this._backgroundColorScale}_generateColorScale(){Q.prototype._generateColorScale.call(this);let e=ji({swatches:1e3,colorKeys:this._colorKeys,colorspace:this._colorspace,shift:1,smooth:this._smooth});e.push(...this.colorKeys);let t=Mi(e.map((e,t)=>({value:Math.round(Oi(e)[2]),index:t})),`value`).map(t=>e[t.index]);return t.length>=101&&(t.length=100,t.push(`#ffffff`)),this._backgroundColorScale=t.map(e=>Pi(e,this._output)),this._backgroundColorScale}},Vi=class{constructor({colors:e,backgroundColor:t,lightness:n,contrast:r=1,saturation:i=100,output:a=`HEX`,formula:o=`wcag2`}){if(this._output=a,this._colors=e,this._lightness=n,this._saturation=i,this._formula=o,this._setBackgroundColor(t),this._setBackgroundColorValue(),this._contrast=r,!this._colors)throw Error(`No colors are defined`);if(!this._backgroundColor)throw Error(`Background color is undefined`);if(e.forEach(e=>{if(!e.ratios)throw Error(`Color ${e.name}'s ratios are undefined`)}),!Ti[this._output])throw Error(`Output “${a}” not supported`);this._saturation<100&&this._updateColorSaturation(this._saturation),this._findContrastColors(),this._findContrastColorPairs(),this._findContrastColorValues()}set formula(e){this._formula=e,this._findContrastColors()}get formula(){return this._formula}set contrast(e){this._contrast=e,this._findContrastColors()}get contrast(){return this._contrast}set lightness(e){this._lightness=e,this._setBackgroundColor(this._backgroundColor),this._findContrastColors()}get lightness(){return this._lightness}set saturation(e){this._saturation=e,this._updateColorSaturation(e),this._findContrastColors()}get saturation(){return this._saturation}set backgroundColor(e){this._setBackgroundColor(e),this._findContrastColors()}get backgroundColorValue(){return this._backgroundColorValue}get backgroundColor(){return this._backgroundColor}set colors(e){this._colors=e,this._findContrastColors()}get colors(){return this._colors}set addColor(e){this._colors.push(e),this._findContrastColors()}set removeColor(e){this._colors=this._colors.filter(t=>t.name!==e.name),this._findContrastColors()}set updateColor(e){if(Array.isArray(e))for(let t=0;tn.name===e[t].color);n=n[0];let r=this._colors.indexOf(n),i=this._colors.filter(n=>n.name!==e[t].color);e[t].name&&(n.name=e[t].name),e[t].colorKeys&&(n.colorKeys=e[t].colorKeys),e[t].ratios&&(n.ratios=e[t].ratios),(e[t].colorSpace!==void 0||e[t].colorspace!==void 0)&&(e[t].colorspace!==void 0&&e[t].colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),n.colorSpace=e[t].colorSpace??e[t].colorspace),e[t].smooth&&(n.smooth=e[t].smooth),n._generateColorScale(),i.splice(r,0,n),this._colors=i}else{let t=this._colors.filter(t=>t.name===e.color);t=t[0];let n=this._colors.indexOf(t),r=this._colors.filter(t=>t.name!==e.color);e.name&&(t.name=e.name),e.colorKeys&&(t.colorKeys=e.colorKeys),e.ratios&&(t.ratios=e.ratios),(e.colorSpace!==void 0||e.colorspace!==void 0)&&(e.colorspace!==void 0&&e.colorSpace===void 0&&console.warn("Leonardo: `colorspace` is deprecated. Use `colorSpace` instead."),t.colorSpace=e.colorSpace??e.colorspace),e.smooth&&(t.smooth=e.smooth),t._generateColorScale(),r.splice(n,0,t),this._colors=r}this._findContrastColors()}set output(e){this._output=e,this._colors.forEach(e=>{e.output=this._output}),this._backgroundColor.output=this._output,this._findContrastColors()}get output(){return this._output}get contrastColors(){return this._contrastColors}get contrastColorPairs(){return this._contrastColorPairs}get contrastColorValues(){return this._contrastColorValues}_setBackgroundColor(e){if(typeof e==`string`){let t=new Bi({name:`background`,colorKeys:[e],output:`RGB`}),n=Z(K(String(e)).hsluv()[2]);this._backgroundColor=t,this._lightness=n,this._backgroundColorValue=t[this._lightness]}else{e.output=`RGB`;let t=e.backgroundColorScale[this._lightness];this._backgroundColor=e,this._backgroundColorValue=t}}_setBackgroundColorValue(){this._backgroundColorValue=this._backgroundColor.backgroundColorScale[this._lightness]}_updateColorSaturation(e){this._colors.map(t=>{t.saturation=e})}_findContrastColors(){let e=K(String(this._backgroundColorValue)).rgb(),t=this._lightness/100,n={background:Pi(this._backgroundColorValue,this._output)},r=[],i=[],a={...n};return r.push(n),this._colors.map(n=>{if(n.ratios!==void 0){let o,s=[],c={name:n.name,values:s},l;Array.isArray(n.ratios)?l=n.ratios:Array.isArray(n.ratios)||(o=Object.keys(n.ratios),l=Object.values(n.ratios)),l=l.map(e=>Ei(+e,this._contrast));let u=zi(n,e,t,l,this._formula).map(e=>Pi(e,this._output));for(let e=0;e{let t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return[Number.parseInt(t[1],16),Number.parseInt(t[2],16),Number.parseInt(t[3],16)]},Wi=(e,t,n)=>{let r=e/255,i=t/255,a=n/255,o=Math.min(r,i,a),s=Math.max(r,i,a),c=s-o,l=0,u=0,d=0;return l=c===0?0:s===r?(i-a)/c%6:s===i?(a-r)/c+2:(r-i)/c+4,l=Math.round(l*60),l<0&&(l+=360),d=(s+o)/2,u=c===0?0:c/(1-Math.abs(2*d-1)),u=+(u*100).toFixed(1),d=+(d*100).toFixed(1),[l,u,Math.round(d)]},Gi=(e,t,n,r)=>{let i=n/100,a=t*Math.min(i,1-i)/100,o=t=>{let n=(t+e/30)%12,r=i-a*Math.max(Math.min(n-3,9-n,1),-1);return Math.round(255*r).toString(16).padStart(2,`0`).toUpperCase()},s=o(0),c=o(8),l=o(4),u=((e,t,n)=>Math.min(Math.max(e,t),n))(r,0,1);return`#${s}${c}${l}${Math.round(u*255).toString(16).padStart(2,`0`).toUpperCase()}`},Ki=(e,t,n=1)=>{let r=Ui(e),i=Ui(t===`white`?`#FFFFFF`:t===`black`?`#000000`:t),a=r.map((e,t)=>[(e-i[t])/(255-i[t]),(e-i[t])/(0-i[t])]),o=Hi(Math.max(...a.flat().filter(e=>/^-?\d+\.?\d*$/.test(e)))),s=r.map((e,t)=>Math.round((e-i[t]+i[t]*o)/o));if(s.includes(NaN)){let e=Wi(r[0],r[1],r[2]);return{h:e[0],s:Math.round(e[1]*n),l:e[2],a:1}}let c=Wi(s[0],s[1],s[2]);return{h:c[0],s:Math.round(c[1]*n),l:c[2],a:o}},qi={backgroundColor:`gray`,colorSpace:`OKLCH`,colorSmoothing:!1,formula:`wcag2`,output:`HEX`,colors:{gray:[$(215,20,90),$(215,8,50),$(215,6,25)],red:[$(358,100,58),$(350,100,30)],orange:[$(32,100,48),$(12,100,30)],yellow:[$(50,100,50),$(25,100,20)],lime:[$(100,68,50),$(115,86,25)],green:[$(163,87,42),$(168,100,25)],cyan:[$(185,80,45),$(200,98,35)],blue:[$(212,98,46),$(222,95,25)],purple:[$(258,94,64),$(265,100,35)],fuchsia:[$(295,56,50),$(285,80,25)],pink:[$(334,90,50),$(330,91,25)]},themes:{light:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16.75],contrast:1,lightness:100,saturation:100},dark:{ratios:[1.03,1.06,1.12,1.25,1.5,1.75,2.25,3.5,5.25,6.5,8,10.5,13.75,16],contrast:1,lightness:6,saturation:97},lightHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:100,saturation:100},darkHc:{ratios:[1.06,1.12,1.25,1.37,1.75,2.25,3.25,4.75,8.87,10,11.75,13.25,16,17],contrast:1,lightness:6,saturation:97}}};function $(e,t,n){return K.hsl(e,t/100,n/100).hex()}function Ji(e,t){let n=e.colorSpace,r=e.colorSmoothing,i=e.themes[t].ratios,a=new Bi({name:`gray`,colorKeys:e.colors.gray,colorspace:n,ratios:i,smooth:r}),o=new Q({name:`blue`,colorKeys:e.colors.blue,colorspace:n,ratios:i,smooth:r}),s=new Q({name:`cyan`,colorKeys:e.colors.cyan,colorspace:n,ratios:i,smooth:r}),c=new Q({name:`fuchsia`,colorKeys:e.colors.fuchsia,colorspace:n,ratios:i,smooth:r}),l=new Q({name:`green`,colorKeys:e.colors.green,colorspace:n,ratios:i,smooth:r}),u=new Q({name:`lime`,colorKeys:e.colors.lime,colorspace:n,ratios:i,smooth:r}),d=new Q({name:`orange`,colorKeys:e.colors.orange,colorspace:n,ratios:i,smooth:r}),f=new Q({name:`pink`,colorKeys:e.colors.pink,colorspace:n,ratios:i,smooth:r}),p=new Q({name:`purple`,colorKeys:e.colors.purple,colorspace:n,ratios:i,smooth:r}),m={gray:a,red:new Q({name:`red`,colorKeys:e.colors.red,colorspace:n,ratios:i,smooth:r}),orange:d,yellow:new Q({name:`yellow`,colorKeys:e.colors.yellow,colorspace:n,ratios:i,smooth:r}),lime:u,green:l,cyan:s,blue:o,purple:p,fuchsia:c,pink:f};return e.colors.custom&&(m.custom=new Q({name:`custom`,colorKeys:e.colors.custom,colorspace:n,ratios:i,smooth:r})),new Vi({colors:Object.values(m),backgroundColor:m[e.backgroundColor],contrast:e.themes[t].contrast,lightness:e.themes[t].lightness,saturation:e.themes[t].saturation,output:e.output,formula:e.formula}).contrastColors}function Yi(e){let t={};for(let n of Object.keys(e.themes))t[n]=Ji(e,n);return t}function Xi(e){qi.colors.custom=[e];let t=Yi(qi);return Object.fromEntries(Object.entries(t).map(([e,t])=>{let n=t.find(e=>e&&e.name===`custom`),r=Object.fromEntries(n.values.map(({name:e,value:t})=>[e,t]));for(let[e,n]of Object.entries(r)){let i=Ki(n,t[0].background);r[`alpha${e.charAt(0).toUpperCase()+e.slice(1)}`]=Gi(i.h,i.s,i.l,i.a)}return[e,r]}))}return e.generateCustomColors=Xi,e.generateThemesJson=Yi,e.hslToHex=$,e.leonardoConfig=qi,e})({}); \ No newline at end of file diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt index c5ac159b5a..5851a758f0 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/CompoundIcons.kt @@ -229,9 +229,6 @@ object CompoundIcons { @Composable fun Filter(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_filter) } - @Composable fun Folder(): ImageVector { - return ImageVector.vectorResource(R.drawable.ic_compound_folder) - } @Composable fun Forward(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_forward) } @@ -771,7 +768,6 @@ object CompoundIcons { FileError(), Files(), Filter(), - Folder(), Forward(), FullScreen(), Grid(), @@ -1000,7 +996,6 @@ object CompoundIcons { R.drawable.ic_compound_file_error, R.drawable.ic_compound_files, R.drawable.ic_compound_filter, - R.drawable.ic_compound_folder, R.drawable.ic_compound_forward, R.drawable.ic_compound_full_screen, R.drawable.ic_compound_grid, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt index 1e3c97d179..f5a54bb7be 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColors.kt @@ -91,6 +91,8 @@ data class SemanticColors( val bgSubtleSecondaryLevel0: Color, /** Subtle background colour for success state elements. State: Rest. */ val bgSuccessSubtle: Color, + /** Accent borders for containers */ + val borderAccentPrimary: Color, /** accent border intended for keylines on message highlights */ val borderAccentSubtle: Color, /** High-contrast border for critical state. State: Hover. */ @@ -171,6 +173,8 @@ data class SemanticColors( val iconTertiary: Color, /** Translucent version of tertiary icon. Refer to it for intended use. */ val iconTertiaryAlpha: Color, + /** Used to separate core sections of the UI as well as containers */ + val separatorPrimary: Color, /** Accent text colour for plain actions. */ val textActionAccent: Color, /** Default text colour for plain actions. */ diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt index adeed8d5a3..35a1234854 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDark.kt @@ -61,6 +61,7 @@ val compoundColorsDark = SemanticColors( bgSubtleSecondary = DarkColorTokens.colorGray300, bgSubtleSecondaryLevel0 = DarkColorTokens.colorThemeBg, bgSuccessSubtle = DarkColorTokens.colorGreen200, + borderAccentPrimary = DarkColorTokens.colorGreen900, borderAccentSubtle = DarkColorTokens.colorGreen700, borderCriticalHovered = DarkColorTokens.colorRed1000, borderCriticalPrimary = DarkColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsDark = SemanticColors( iconSuccessPrimary = DarkColorTokens.colorGreen900, iconTertiary = DarkColorTokens.colorGray800, iconTertiaryAlpha = DarkColorTokens.colorAlphaGray800, + separatorPrimary = DarkColorTokens.colorGray400, textActionAccent = DarkColorTokens.colorGreen900, textActionPrimary = DarkColorTokens.colorGray1400, textBadgeAccent = DarkColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt index 9bab04f833..802e3d56b0 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsDarkHc.kt @@ -61,6 +61,7 @@ val compoundColorsHcDark = SemanticColors( bgSubtleSecondary = DarkHcColorTokens.colorGray300, bgSubtleSecondaryLevel0 = DarkHcColorTokens.colorThemeBg, bgSuccessSubtle = DarkHcColorTokens.colorGreen200, + borderAccentPrimary = DarkHcColorTokens.colorGreen900, borderAccentSubtle = DarkHcColorTokens.colorGreen700, borderCriticalHovered = DarkHcColorTokens.colorRed1000, borderCriticalPrimary = DarkHcColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsHcDark = SemanticColors( iconSuccessPrimary = DarkHcColorTokens.colorGreen900, iconTertiary = DarkHcColorTokens.colorGray800, iconTertiaryAlpha = DarkHcColorTokens.colorAlphaGray800, + separatorPrimary = DarkHcColorTokens.colorGray400, textActionAccent = DarkHcColorTokens.colorGreen900, textActionPrimary = DarkHcColorTokens.colorGray1400, textBadgeAccent = DarkHcColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt index 033efc63ef..166f9ddafc 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLight.kt @@ -61,6 +61,7 @@ val compoundColorsLight = SemanticColors( bgSubtleSecondary = LightColorTokens.colorGray300, bgSubtleSecondaryLevel0 = LightColorTokens.colorGray300, bgSuccessSubtle = LightColorTokens.colorGreen200, + borderAccentPrimary = LightColorTokens.colorGreen900, borderAccentSubtle = LightColorTokens.colorGreen700, borderCriticalHovered = LightColorTokens.colorRed1000, borderCriticalPrimary = LightColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsLight = SemanticColors( iconSuccessPrimary = LightColorTokens.colorGreen900, iconTertiary = LightColorTokens.colorGray800, iconTertiaryAlpha = LightColorTokens.colorAlphaGray800, + separatorPrimary = LightColorTokens.colorGray400, textActionAccent = LightColorTokens.colorGreen900, textActionPrimary = LightColorTokens.colorGray1400, textBadgeAccent = LightColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt index c1f677d156..8914b41bfc 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/SemanticColorsLightHc.kt @@ -61,6 +61,7 @@ val compoundColorsHcLight = SemanticColors( bgSubtleSecondary = LightHcColorTokens.colorGray300, bgSubtleSecondaryLevel0 = LightHcColorTokens.colorGray300, bgSuccessSubtle = LightHcColorTokens.colorGreen200, + borderAccentPrimary = LightHcColorTokens.colorGreen900, borderAccentSubtle = LightHcColorTokens.colorGreen700, borderCriticalHovered = LightHcColorTokens.colorRed1000, borderCriticalPrimary = LightHcColorTokens.colorRed900, @@ -101,6 +102,7 @@ val compoundColorsHcLight = SemanticColors( iconSuccessPrimary = LightHcColorTokens.colorGreen900, iconTertiary = LightHcColorTokens.colorGray800, iconTertiaryAlpha = LightHcColorTokens.colorAlphaGray800, + separatorPrimary = LightHcColorTokens.colorGray400, textActionAccent = LightHcColorTokens.colorGreen900, textActionPrimary = LightHcColorTokens.colorGray1400, textBadgeAccent = LightHcColorTokens.colorGreen1100, diff --git a/libraries/dateformatter/impl/src/main/res/values-ja/translations.xml b/libraries/dateformatter/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..d044992377 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s %2$s" + "今月" + diff --git a/libraries/dateformatter/impl/src/main/res/values-vi/translations.xml b/libraries/dateformatter/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..b846434e9e --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s lúc %2$s" + "Tháng này" + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt index 85307823f6..f70ed3b344 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ExpandableBottomSheetLayout.kt @@ -16,7 +16,10 @@ import android.widget.EditText import androidx.appcompat.app.ActionBar.LayoutParams import androidx.compose.animation.core.Animatable import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.awaitVerticalPointerSlopOrCancellation +import androidx.compose.foundation.gestures.verticalDrag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column @@ -41,10 +44,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview @@ -94,7 +101,7 @@ fun ExpandableBottomSheetLayout( .run { if (isSwipeGestureEnabled) { pointerInput(maxBottomSheetContentHeight) { - detectVerticalDragGestures( + customDetectVerticalDragGestures( onVerticalDrag = { _, dragAmount -> val calculatedHeight = max(minBottomContentHeightPx, currentBottomContentHeightPx - dragAmount.roundToInt()) val newHeight = min(calculatedMaxBottomContentHeightPx, calculatedHeight) @@ -120,7 +127,11 @@ fun ExpandableBottomSheetLayout( animatable.animateTo(destination) } - } + }, + canScroll = { + // We only consider we can scroll in the contents if the min size matches the max size so it's maximized + minBottomContentHeightPx == calculatedMaxBottomContentHeightPx + }, ) } } else { @@ -189,6 +200,45 @@ fun ExpandableBottomSheetLayout( ) } +// The original detectVerticalDragGestures doesn't allow us to conditionally consume the initial slop event that triggers the drag, +// which is necessary in our case to allow inner scrollables to work when the sheet is not fully expanded, so we need to re-implement it here +private suspend fun PointerInputScope.customDetectVerticalDragGestures( + onDragStart: (Offset) -> Unit = {}, + onDragEnd: () -> Unit = {}, + onDragCancel: () -> Unit = {}, + canScroll: () -> Boolean = { false }, + onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit, +) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + var overSlop = 0f + val drag = + awaitVerticalPointerSlopOrCancellation(down.id, down.type) { change, over -> + // Consuming this event is what triggers the dragging instead of the inner content scrolling + // We should only consume it if we can't scroll in the inner content so we drag the bottom sheet instead, otherwise we let it pass through + // This is the only change compared to the original detectVerticalDragGestures implementation + if (!canScroll()) { + change.consume() + } + overSlop = over + } + if (drag != null) { + onDragStart.invoke(drag.position) + onVerticalDrag.invoke(drag, overSlop) + if ( + verticalDrag(drag.id) { + onVerticalDrag(it, it.positionChange().y) + it.consume() + } + ) { + onDragEnd() + } else { + onDragCancel() + } + } + } +} + @Preview(showBackground = true) @Composable @Suppress("UnusedPrivateMember") diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPin.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPin.kt index af8e29d518..9e783d605c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPin.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPin.kt @@ -32,9 +32,13 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.withSave import coil3.Image import coil3.ImageLoader @@ -50,6 +54,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.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.utils.CommonDrawables private val PIN_WIDTH = 42.dp private val PIN_HEIGHT = PIN_WIDTH * 1.2f @@ -99,21 +104,33 @@ fun LocationPin( fun rememberLocationPinBitmap(variant: PinVariant): ImageBitmap? { val context = LocalContext.current val density = LocalDensity.current - val imageLoader = SingletonImageLoader.get(context) val colors = pinColors(variant) val cacheKey = rememberCacheKey(variant) - return produceState(initialValue = null, cacheKey) { - val memoryCacheKey = MemoryCache.Key(cacheKey) - val cached = imageLoader.memoryCache?.get(memoryCacheKey) - if (cached != null) { - value = cached.image.toBitmap().asImageBitmap() - } else { - val dimensions = PinDimensions(density) - val bitmap = LocationPinRenderer.renderPin(variant, colors, dimensions, context, imageLoader) - imageLoader.memoryCache?.set(memoryCacheKey, MemoryCache.Value(bitmap.asImage())) - value = bitmap.asImageBitmap() - } - }.value + val resources = LocalResources.current + + return if (LocalInspectionMode.current) { + // In preview mode, skip async loading and return a simple placeholder image instead to avoid using ImageLoader + val dimensions = PinDimensions(density) + val avatarImage = ResourcesCompat.getDrawable(resources, CommonDrawables.sample_avatar, context.theme)?.toBitmap()?.asImage() + LocationPinRenderer.renderPin(variant, colors, dimensions, avatarImage).asImageBitmap() + } else { + produceState(initialValue = null, cacheKey) { + val imageLoader = SingletonImageLoader.get(context) + val memoryCacheKey = MemoryCache.Key(cacheKey) + val cached = imageLoader.memoryCache?.get(memoryCacheKey) + if (cached != null) { + value = cached.image.toBitmap().asImageBitmap() + } else { + val dimensions = PinDimensions(density) + val bitmap = with(LocationPinRenderer) { + val avatarImage = loadAvatarImage(variant, context, imageLoader) + renderPin(variant, colors, dimensions, avatarImage) + } + imageLoader.memoryCache?.set(memoryCacheKey, MemoryCache.Value(bitmap.asImage())) + value = bitmap.asImageBitmap() + } + }.value + } } @Composable @@ -208,19 +225,17 @@ private object LocationPinRenderer { /** * Renders a pin variant to bitmap. Suspending for async avatar loading. */ - suspend fun renderPin( + fun renderPin( variant: PinVariant, colors: PinColors, dimensions: PinDimensions, - context: Context, - imageLoader: ImageLoader, + avatarImage: Image?, ): Bitmap { val bitmap = createBitmap(dimensions.pinWidth.toInt(), dimensions.pinHeight.toInt()) val canvas = Canvas(bitmap) canvas.drawPinShape(colors.fill, colors.stroke, dimensions) when (variant) { is PinVariant.UserLocation -> { - val avatarImage = loadAvatarImage(variant.avatarData, context, imageLoader) canvas.drawAvatar( avatarImage = avatarImage, avatarData = variant.avatarData, @@ -284,11 +299,15 @@ private object LocationPinRenderer { return path } - private suspend fun loadAvatarImage( - avatarData: AvatarData, + suspend fun loadAvatarImage( + variant: PinVariant, context: Context, imageLoader: ImageLoader, ): Image? { + val avatarData = when (variant) { + is PinVariant.UserLocation -> variant.avatarData + else -> return null + } val request = ImageRequest.Builder(context) .data(avatarData) // Disable hardware rendering for Canvas diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 53d5a7c281..b1e3356fc3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -13,10 +13,12 @@ import androidx.compose.ui.unit.dp enum class AvatarSize(val dp: Dp) { CurrentUserTopBar(32.dp), + CurrentRoomTopBar(32.dp), IncomingCall(140.dp), RoomDetailsHeader(96.dp), RoomListItem(52.dp), + ThreadsListItem(52.dp), SpaceListItem(52.dp), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/BitmapAvatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/BitmapAvatar.kt new file mode 100644 index 0000000000..f25174b7b0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/BitmapAvatar.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.components.avatar + +import android.graphics.Bitmap +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import coil3.compose.AsyncImagePainter +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent +import io.element.android.libraries.designsystem.components.avatar.internal.InitialLetterAvatar +import timber.log.Timber + +// For user avatar only. +@Composable +fun BitmapAvatar( + avatarData: AvatarData, + bitmap: Bitmap?, + modifier: Modifier = Modifier, + contentDescription: String? = null, +) { + val avatarShape = AvatarType.User.avatarShape() + when { + bitmap == null -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = null, + modifier = modifier, + contentDescription = contentDescription, + ) + else -> { + val size = avatarData.size.dp + SubcomposeAsyncImage( + model = bitmap, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = modifier + .size(size) + .clip(avatarShape) + ) { + val collectedState by painter.state.collectAsState() + when (val state = collectedState) { + is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() + is AsyncImagePainter.State.Error -> { + SideEffect { + Timber.e( + state.result.throwable, + "Error loading avatar $state\n${state.result}" + ) + } + InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = null, + contentDescription = contentDescription, + ) + } + else -> InitialLetterAvatar( + avatarData = avatarData, + avatarShape = avatarShape, + forcedAvatarSize = null, + contentDescription = contentDescription, + ) + } + } + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index 06827fb218..8973e312ce 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -75,6 +75,9 @@ val SemanticColors.pinnedMessageBannerIndicator val SemanticColors.pinnedMessageBannerBorder get() = if (isLight) LightColorTokens.colorAlphaGray400 else DarkColorTokens.colorAlphaGray400 +val SemanticColors.floatingDateBadgeBackground + get() = if (isLight) bgCanvasDefault else bgSubtlePrimary + @PreviewsDayNight @Composable internal fun ColorAliasesPreview() = ElementPreview { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowSizeClassUtils.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowSizeClassUtils.kt new file mode 100644 index 0000000000..ba0752e8a2 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/WindowSizeClassUtils.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.utils + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass +import androidx.compose.runtime.Composable + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun hasCompactHeightWindowSize(): Boolean { + return currentWindowAdaptiveInfo().windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt index f9d38fd8a7..5abfa89ef0 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt @@ -118,15 +118,6 @@ class StateContentFormatter( "PolicyRuleUser" } } - OtherState.RoomAliases -> when (renderingMode) { - RenderingMode.RoomList -> { - Timber.v("Filtering timeline item for room state change: $content") - null - } - RenderingMode.Timeline -> { - "RoomAliases" - } - } OtherState.RoomCanonicalAlias -> when (renderingMode) { RenderingMode.RoomList -> { Timber.v("Filtering timeline item for room state change: $content") diff --git a/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..8613aae2df --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,73 @@ + + + "(アバターも変更)" + "%1$s がアバターを変更" + "あなたがアバターを変更" + "%1$s がメンバーに降格" + "%1$s がモデレーターに降格" + "%1$sが表示名を変更: %2$s > %3$s" + "あなたが表示名を変更: %1$s > %2$s" + "%1$sが表示名を削除 (%2$s)" + "表示名を削除 (%1$s)" + "%1$sが表示名を設定: %2$s" + "あなたが表示名を設定: %1$s" + "%1$s が管理者に昇格" + "%1$s がモデレーターに昇格" + "%1$sがルームアバターを変更" + "あなたがルームアバターを変更" + "%1$sがルームアバターを削除" + "あなたがルームアバターを削除" + "%1$s が %2$s を追放" + "あなたが %1$s を追放" + "あなたが %1$s を追放: %2$s" + "%1$s が %2$s を追放: %3$s" + "%1$s がルームを作成" + "あなたがルームを作成" + "%1$s が %2$s を招待" + "%1$s が招待を受諾" + "あなたが招待を受諾" + "あなたが %1$s を招待" + "%1$s があなたを招待" + "%1$s がルームに参加" + "あなたがルームに参加" + "%1$s が参加をリクエスト" + "%1$s が %2$s の参加を許可" + "あなたが %1$s の参加を許可" + "あなたが参加をリクエスト" + "%1$s が %2$s の参加リクエストを拒否" + "あなたが %1$s の参加リクエストを拒否" + "%1$s があなたの参加リクエストを拒否" + "%1$s が参加リクエストを取り消し" + "あなたが参加リクエストを取り消し" + "%1$s がルームを退出" + "あなたがルームを退出" + "%1$s がルーム名を変更: %2$s" + "あなたがルーム名を変更: %1$s" + "%1$s がルーム名を削除" + "あなたがルーム名を削除" + "%1$s による変更はありません" + "あなたによる変更はありません" + "%1$s はピン留めメッセージを変更しました" + "あなたがピン留めメッセージを変更しました" + "%1$s がメッセージをピン留め" + "あなたがメッセージをピン留め" + "%1$s がメッセージのピン留めを解除" + "あなたがメッセージのピン留めを解除" + "%1$s が招待を拒否" + "あなたが招待を拒否" + "%1$s が %2$s を削除" + "あなたが %1$s を削除" + "あなたが%1$s を削除: %2$s" + "%1$s が %2$s を削除: %3$s" + "%1$s が %2$s をルームに招待" + "あなたが %1$s をルームに招待" + "%1$s が %2$s へのルームの招待を取り消し" + "あなたが %1$s へのルームの招待を取り消し" + "%1$s がトピックを変更: %2$s" + "あなたがトピックを変更: %1$s" + "%1$s がルームのトピックを削除" + "あなたがルームのトピックを削除" + "%1$s が %2$s の追放を解除" + "あなたが %1$s の追放を解除" + "%1$s がメンバーシップに未知の変更を追加" + diff --git a/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml b/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml index 9fb1486d59..9a632b8f5a 100644 --- a/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-lt/translations.xml @@ -1,13 +1,13 @@ "(taip pat buvo pakeistas ir avataras)" - "%1$s pakeitė savo avatarą" - "Jūs pakeitėte savo avatarą" + "%1$s pakeitė savo pseudoportretą" + "Jūs pakeitėte savo pseudoportretą" "%1$s pakeitė savo slapyvardį iš %2$s į %3$s" "Jūs pakeitėte savo slapyvardį iš %1$s į %2$s" "%1$s pašalino savo slapyvardį (jis buvo %2$s)" "Jūs pašalinote savo slapyvardį (jis buvo %1$s)" - "%1$s pakeitė savo slapyvardį į %2$s" + "%1$s nustatė savo rodomą vardą į %2$s" "Jūs nustatėte savo slapyvardį į %1$s" "%1$s pakeitė kambario avatarą" "Jūs pakeitėte kambario avatarą" @@ -21,7 +21,7 @@ "%1$s priėmė kvietimą" "Priėmėte kvietimą" "Jūs pakvietėte %1$s" - "%1$s pakvietė Jus" + "%1$s pakvietė jus" "%1$s prisijungė prie kambario" "Jūs prisijungėte prie kambario" "%1$s prašo prisijungti" diff --git a/libraries/eventformatter/impl/src/main/res/values-vi/translations.xml b/libraries/eventformatter/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..7598ba0cb9 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,64 @@ + + + "(ảnh hồ sơ cũng được thay)" + "%1$s đổi ảnh hồ sơ" + "Bạn đổi ảnh hồ sơ" + "%1$s bị giáng cấp xuống thành thành viên" + "%1$s bị giáng chức xuống làm người điều hành" + "%1$s đổi tên hiển thị từ %2$s sang %3$s" + "Bạn đổi tên hiển thị từ %1$s sang %2$s" + "%1$s xoá tên hiển thị (trước kia là %2$s)" + "Bạn xoá tên hiển thị (trước kia là %1$s)" + "%1$s đặt tên hiển thị thành %2$s" + "Bạn đặt tên hiển thị thành %1$s" + "%1$s đã được thăng chức lên quản trị viên" + "%1$s đã được thăng chức lên làm người điều hành" + "%1$s đổi ảnh phòng" + "Bạn đổi ảnh phòng" + "%1$s đã xóa ảnh đại diện của phòng." + "Bạn đã xóa hình đại diện của phòng trò chuyện" + "%1$s cấm %2$s vào phòng" + "Bạn cấm %1$s vào phòng" + "%1$s tạo phòng này" + "Bạn tạo phòng này" + "%1$s mời %2$s" + "%1$s đã chấp nhận lời mời" + "Bạn đã chấp nhận lời mời" + "Bạn mời %1$s" + "%1$s mời bạn" + "%1$s vào phòng" + "Bạn vào phòng" + "%1$s đang yêu cầu tham gia" + "%1$s được cấp quyền truy cập vào %2$s" + "Bạn đã cho phép %1$s tham gia" + "Bạn đã yêu cầu tham gia" + "%1$s đã từ chối yêu cầu tham gia của %2$s" + "Bạn đã từ chối yêu cầu tham gia của %1$s" + "%1$s đã từ chối yêu cầu tham gia của bạn" + "%1$s không còn mong muốn tham gia" + "Bạn đã hủy yêu cầu tham gia" + "%1$s rời phòng" + "Bạn rời phòng" + "%1$s đổi tên phòng thành %2$s" + "Bạn đổi tên phòng thành %1$s" + "%1$s xóa tên phòng" + "Bạn xóa tên phòng" + "%1$s không có thay đổi nào" + "Bạn chưa thực hiện thay đổi nào" + "Bạn đã thay đổi tin nhắn được ghim" + "%1$s từ chối lời mời" + "Bạn từ chối lời mời" + "%1$s cho %2$s cút khỏi phòng" + "Bạn cho %1$s cút khỏi phòng" + "%1$s đã gửi lời mời đến %2$s để tham gia phòng trò chuyện" + "Bạn đã gửi lời mời đến %1$s để tham gia phòng trò chuyện" + "%1$s đã thu hồi lời mời tham gia phòng trò chuyện của %2$s " + "Bạn đã thu hồi lời mời tham gia phòng trò chuyện của %1$s " + "%1$s đổi chủ đề sang: %2$s" + "Bạn đổi chủ đề sang: %1$s" + "%1$s đã xóa chủ đề phòng" + "Bạn đã xóa chủ đề của phòng." + "%1$s hủy lệnh cấm với %2$s" + "Bạn hủy lệnh cấm với %1$s" + "%1$s đã thực hiện một thay đổi không xác định đối với tư cách thành viên của họ" + diff --git a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml index 9776510d6f..cd22ebed38 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml @@ -54,11 +54,11 @@ "%1$s 取消置顶了一条消息" "您取消置顶了一条消息" "%1$s 拒绝了邀请" - "你拒绝了邀请" - "%1$s 移除了 %2$s" - "你移除了 %1$s" - "您已删除%1$s :%2$s" - "%1$s已移除%2$s:%3$s" + "您拒绝了邀请" + "%1$s 已移除 %2$s" + "您移除了 %1$s" + "您移除了 %1$s:%2$s" + "%1$s 移除了 %2$s:%3$s" "%1$s 向 %2$s 发送了加入聊天室的邀请" "你邀请 %1$s 加入聊天室" "%1$s 撤销了 %2$s 加入聊天室的邀请" diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt index e91bed409e..38334afad5 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt @@ -601,7 +601,6 @@ class DefaultPinnedMessagesBannerFormatterTest { OtherState.PolicyRuleRoom, OtherState.PolicyRuleServer, OtherState.PolicyRuleUser, - OtherState.RoomAliases, OtherState.RoomCanonicalAlias, OtherState.RoomGuestAccess, OtherState.RoomHistoryVisibility, diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt index 2345af8a33..e1e8717c4c 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatterTest.kt @@ -746,7 +746,6 @@ class DefaultRoomLatestEventFormatterTest { OtherState.PolicyRuleRoom, OtherState.PolicyRuleServer, OtherState.PolicyRuleUser, - OtherState.RoomAliases, OtherState.RoomCanonicalAlias, OtherState.RoomGuestAccess, OtherState.RoomHistoryVisibility, diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 6cd9dec60c..eaa32e8adc 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -70,27 +70,6 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), - CreateSpaces( - key = "feature.createSpaces", - title = "Create spaces", - description = "Allow creating spaces.", - defaultValue = { true }, - isFinished = false, - ), - SpaceSettings( - key = "feature.spaceSettings", - title = "Space settings", - description = "Allow managing space settings such as details, permissions and privacy.", - defaultValue = { true }, - isFinished = false, - ), - RoomListSpaceFilters( - key = "feature.roomListSpaceFilters", - title = "Room list space filters", - description = "Allow filtering the room list by space.", - defaultValue = { true }, - isFinished = false, - ), PrintLogsToLogcat( key = "feature.print_logs_to_logcat", title = "Print logs to logcat", @@ -156,10 +135,31 @@ enum class FeatureFlags( ), ValidateNetworkWhenSchedulingNotificationFetching( key = "feature.validate_network_when_scheduling_notification_fetching", - title = "validate internet connectivity when scheduling notification fetching", + title = "Validate internet connectivity when scheduling notification fetching", description = "Only fetch events for push notifications when the device has internet connectivity. " + "Enabling this can be problematic in air-gapped environments.", defaultValue = { true }, isFinished = false, ), + FloatingDateBadge( + key = "feature.floating_date_badge", + title = "Display sticky date headers in the timeline", + description = "When scrolling, a sticky date badge will be displayed so you can easily know on which date the messages you're seeing were sent.", + defaultValue = { false }, + isFinished = false, + ), + SlashCommand( + key = "feature.slash_command", + title = "Parse slash commands in the message composer", + description = "Allow parsing slash commands in the message composer and perform action.", + defaultValue = { false }, + isFinished = false, + ), + RoomThreadList( + key = "feature.room_thread_list", + title = "Add a list of threads in a room", + description = "Add a new screen with a list of threads in a room.", + defaultValue = { false }, + isFinished = false, + ), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt new file mode 100644 index 0000000000..e914656f5f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api + +/** + * Provides information about the capabilities of the homeserver. + * + * Spec: https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation + */ +interface HomeserverCapabilitiesProvider { + /** + * Manually refresh the capabilities of the homeserver performing a network request. + */ + suspend fun refresh(): Result + + /** + * Indicates whether the homeserver allows the user to change their display name. + */ + suspend fun canChangeDisplayName(): Result + + /** + * Indicates whether the homeserver allows the user to change their avatar URL. + */ + suspend fun canChangeAvatarUrl(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index f5c235da68..7c35663832 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -225,6 +225,8 @@ interface MatrixClient { * Resets the cached client `well-known` config by the SDK. */ suspend fun resetWellKnownConfig(): Result + + fun homeserverCapabilities(): HomeserverCapabilitiesProvider } /** diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ConfirmingLoginWithElementClassic.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/ElementClassicSession.kt similarity index 56% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ConfirmingLoginWithElementClassic.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/ElementClassicSession.kt index 5fae0afdd5..d094019db2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/classic/ConfirmingLoginWithElementClassic.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/ElementClassicSession.kt @@ -5,11 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.screens.onboarding.classic +package io.element.android.libraries.matrix.api.auth -import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.UserId -class ConfirmingLoginWithElementClassic( +data class ElementClassicSession( val userId: UserId, -) : AsyncAction.Confirming + val homeserverUrl: String?, + val secrets: String?, + val roomKeysVersion: String?, + val doesContainBackupKey: Boolean, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index 1c574ad467..7c82668242 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId interface MatrixAuthenticationService { /** @@ -52,6 +53,20 @@ interface MatrixAuthenticationService { */ suspend fun cancelOidcLogin(): Result + /** + * Set the existing data about Element Classic session, if any. + */ + fun setElementClassicSession(session: ElementClassicSession?) + + /** + * Check if the provided secrets from Element Classic session contain a key backup. + */ + fun doSecretsContainBackupKey( + userId: UserId, + secrets: String, + backupInfo: String, + ): Boolean + /** * Attempt to login using the [callbackUrl] provided by the Oidc page. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomIdOrAlias.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomIdOrAlias.kt index 4ac480e064..462ec0535c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomIdOrAlias.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomIdOrAlias.kt @@ -27,6 +27,16 @@ sealed interface RoomIdOrAlias : Parcelable { is Id -> roomId.value is Alias -> roomAlias.value } + + companion object { + fun from(id: String): RoomIdOrAlias? { + return when { + MatrixPatterns.isRoomId(id) -> Id(RoomId(id)) + MatrixPatterns.isRoomAlias(id) -> Alias(RoomAlias(id)) + else -> null + } + } + } } fun RoomId.toRoomIdOrAlias() = RoomIdOrAlias.Id(this) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt index 306ab8354b..09ceaa4712 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt @@ -17,3 +17,18 @@ interface MxcTools { */ fun mxcUri2FilePath(mxcUri: String): String? } + +/** + * "mxc" scheme, including "://". So "mxc://". + */ +const val MATRIX_CONTENT_URI_SCHEME = "mxc://" + +/** + * Return true if the String starts with "mxc://". + */ +fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME) + +/** + * Remove the "mxc://" prefix. No op if the String is not a Mxc URL. + */ +fun String.removeMxcPrefix() = removePrefix(MATRIX_CONTENT_URI_SCHEME) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 5dce175237..c58a458865 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -95,7 +95,6 @@ sealed interface NotificationContent { data object PolicyRuleRoom : StateEvent data object PolicyRuleServer : StateEvent data object PolicyRuleUser : StateEvent - data object RoomAliases : StateEvent data object RoomAvatar : StateEvent data object RoomCanonicalAlias : StateEvent data object RoomCreate : StateEvent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt index 808f37c7c9..32a6f2e409 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.LiveLocationShare import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver @@ -44,6 +45,8 @@ interface JoinedRoom : BaseRoom { */ val liveTimeline: Timeline + val threadsListService: ThreadsListService + /** * Create a new timeline. * @param createTimelineParams contains parameters about how to filter the timeline. Will also configure the date separators. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt index 41d64afff1..705dd25122 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt @@ -13,7 +13,6 @@ sealed interface StateEventType { data object PolicyRuleServer : StateEventType data object PolicyRuleUser : StateEventType data object CallMember : StateEventType - data object RoomAliases : StateEventType data object RoomAvatar : StateEventType data object RoomCanonicalAlias : StateEventType data object RoomCreate : StateEventType diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListItem.kt new file mode 100644 index 0000000000..8282caafd1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListItem.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.room.threads + +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.core.toThreadId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails + +@Immutable +data class ThreadListItem( + val rootEvent: ThreadListItemEvent, + val latestEvent: ThreadListItemEvent?, + val numberOfReplies: Long, +) { + val threadId = rootEvent.eventId.toThreadId() +} + +@Immutable +data class ThreadListItemEvent( + val eventId: EventId, + val senderId: UserId, + val senderProfile: ProfileDetails, + val isOwn: Boolean, + val content: EventContent?, + val timestamp: Long, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListPaginationStatus.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListPaginationStatus.kt new file mode 100644 index 0000000000..0716ca7c11 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadListPaginationStatus.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.room.threads + +sealed interface ThreadListPaginationStatus { + data class Idle( + val hasMoreToLoad: Boolean, + ) : ThreadListPaginationStatus + + data object Loading : ThreadListPaginationStatus +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt new file mode 100644 index 0000000000..7f819c540c --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.room.threads + +import kotlinx.coroutines.flow.Flow + +interface ThreadsListService { + fun subscribeToItemUpdates(): Flow> + fun subscribeToPaginationUpdates(): Flow + suspend fun paginate(): Result + suspend fun reset(): Result + fun destroy() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MsgType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MsgType.kt new file mode 100644 index 0000000000..b8d3933663 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MsgType.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.timeline + +enum class MsgType { + MSG_TYPE_TEXT, + MSG_TYPE_EMOTE, + + // For future support + MSG_TYPE_SNOW, + + // For future support + MSG_TYPE_CONFETTI, +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 8e04e452b9..670ed8bb9c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -69,6 +69,8 @@ interface Timeline : AutoCloseable { body: String, htmlBody: String?, intentionalMentions: List, + msgType: MsgType = MsgType.MSG_TYPE_TEXT, + asPlainText: Boolean = false, ): Result /** @@ -102,6 +104,7 @@ interface Timeline : AutoCloseable { htmlBody: String?, intentionalMentions: List, fromNotification: Boolean = false, + msgType: MsgType = MsgType.MSG_TYPE_TEXT, ): Result suspend fun sendImage( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt index ed3f53169f..8b4a7eaa13 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt @@ -16,7 +16,6 @@ sealed interface OtherState { data object PolicyRuleRoom : OtherState data object PolicyRuleServer : OtherState data object PolicyRuleUser : OtherState - data object RoomAliases : OtherState data class RoomAvatar(val url: String?) : OtherState data object RoomCanonicalAlias : OtherState data object RoomCreate : OtherState diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/core/UserIdTest.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/core/UserIdTest.kt new file mode 100644 index 0000000000..eb97ce0d8c --- /dev/null +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/core/UserIdTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.core + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class UserIdTest { + @Test + fun `valid user id`() { + val userId = UserId("@alice:example.org") + assertThat(userId.extractedDisplayName).isEqualTo("alice") + assertThat(userId.domainName).isEqualTo("example.org") + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProvider.kt new file mode 100644 index 0000000000..d82e389aa7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider +import org.matrix.rustcomponents.sdk.HomeserverCapabilities + +class RustHomeserverCapabilitiesProvider( + private val homeserverCapabilities: HomeserverCapabilities, +) : HomeserverCapabilitiesProvider { + override suspend fun refresh(): Result = runCatchingExceptions { + homeserverCapabilities.refresh() + } + + override suspend fun canChangeDisplayName(): Result = runCatchingExceptions { + homeserverCapabilities.canChangeDisplayname() + } + + override suspend fun canChangeAvatarUrl(): Result = runCatchingExceptions { + homeserverCapabilities.canChangeAvatar() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 945841f148..3b667c110b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes import io.element.android.libraries.matrix.api.core.DeviceId @@ -838,6 +839,10 @@ class RustMatrixClient( val request = PerformDatabaseVacuumRequestBuilder(sessionId) sessionCoroutineScope.launch { workManagerScheduler.submit(request) } } + + override fun homeserverCapabilities(): HomeserverCapabilitiesProvider { + return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities()) + } } private fun defaultRoomCreationPowerLevels(isPublic: Boolean, isSpace: Boolean) = PowerLevels( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 7cd9fbedf5..9fe7c7cd1f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.AuthenticationException +import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -25,6 +26,7 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync import io.element.android.libraries.matrix.impl.RustMatrixClientFactory @@ -50,6 +52,7 @@ import org.matrix.rustcomponents.sdk.QrCodeData import org.matrix.rustcomponents.sdk.QrCodeDecodeException import org.matrix.rustcomponents.sdk.QrLoginProgress import org.matrix.rustcomponents.sdk.QrLoginProgressListener +import org.matrix.rustcomponents.sdk.SecretsBundleWithUserId import timber.log.Timber import uniffi.matrix_sdk.OAuthAuthorizationData import kotlin.time.Duration.Companion.seconds @@ -64,6 +67,9 @@ class RustMatrixAuthenticationService( private val passphraseGenerator: PassphraseGenerator, private val oidcConfigurationProvider: OidcConfigurationProvider, ) : MatrixAuthenticationService { + // Any existing Element Classic session that we want to try to import secrets from during login. + private var elementClassicSession: ElementClassicSession? = null + // Passphrase which will be used for new sessions. Existing sessions will use the passphrase // stored in the SessionData. private val pendingPassphrase = getDatabasePassphrase() @@ -138,9 +144,15 @@ class RustMatrixAuthenticationService( runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") - client.login(username, password, "Element X Android", null) + client.login( + username = username, + password = password, + initialDeviceName = "Element X Android", + deviceId = null, + ) // Ensure that the user is not already logged in with the same account ensureNotAlreadyLoggedIn(client) + tryToImportSecretForElementClassicSession(client) val sessionData = client.session() .toSessionData( isTokenValid = true, @@ -162,6 +174,53 @@ class RustMatrixAuthenticationService( } } + private suspend fun tryToImportSecretForElementClassicSession(client: Client) { + elementClassicSession + ?.takeIf { + // Note: the SDK will also do this check + it.userId.value == client.userId() + } + ?.let { + val secrets = it.secrets + val roomKeysVersion = it.roomKeysVersion + if (secrets == null || roomKeysVersion == null) { + Timber.d("No secrets or roomKeysVersion found for Element Classic session ${it.userId}, skipping import") + } else { + Timber.d("Trying to import secrets for Element Classic session ${it.userId}") + runCatchingExceptions { + SecretsBundleWithUserId.fromStr( + userId = it.userId.value, + bundle = secrets, + backupInfo = roomKeysVersion, + ).use { secretsBundle -> + client.encryption().importSecretsBundle(secretsBundle) + } + }.onFailure { failure -> + Timber.e(failure, "Failed to import secrets for Element Classic session ${it.userId}") + } + } + } + } + + override fun doSecretsContainBackupKey( + userId: UserId, + secrets: String, + backupInfo: String, + ): Boolean { + return try { + SecretsBundleWithUserId.fromStr( + userId = userId.value, + bundle = secrets, + backupInfo = backupInfo, + ).use { secretsBundle -> + secretsBundle.containsBackupKey() + } + } catch (failure: Exception) { + Timber.e(failure, "Failed to parse secrets for Element Classic session $userId") + false + } + } + override suspend fun importCreatedSession(externalSession: ExternalSession): Result = withContext(coroutineDispatchers.io) { runCatchingExceptions { @@ -233,6 +292,10 @@ class RustMatrixAuthenticationService( } } + override fun setElementClassicSession(session: ElementClassicSession?) { + elementClassicSession = session + } + /** * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters). */ @@ -241,14 +304,15 @@ class RustMatrixAuthenticationService( runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") - client.loginWithOidcCallback(callbackUrl) - + client.loginWithOidcCallback( + callbackUrl = callbackUrl, + ) // Free the pending data since we won't use it to abort the flow anymore pendingOAuthAuthorizationData?.close() pendingOAuthAuthorizationData = null - // Ensure that the user is not already logged in with the same account ensureNotAlreadyLoggedIn(client) + tryToImportSecretForElementClassicSession(client) val sessionData = client.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 6ca7d27a8f..bef24003b9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -13,6 +13,7 @@ import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -90,4 +91,9 @@ object SessionMatrixModule { fun providesSpaceService(matrixClient: MatrixClient): SpaceService { return matrixClient.spaceService } + + @Provides + fun providesHomeserverCapabilitiesProvider(matrixClient: MatrixClient): HomeserverCapabilitiesProvider { + return matrixClient.homeserverCapabilities() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index 96c4bdf3c4..7e65a1cc5c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -49,7 +49,6 @@ private fun StateEventContent.toContent(): NotificationContent.StateEvent { StateEventContent.PolicyRuleRoom -> NotificationContent.StateEvent.PolicyRuleRoom StateEventContent.PolicyRuleServer -> NotificationContent.StateEvent.PolicyRuleServer StateEventContent.PolicyRuleUser -> NotificationContent.StateEvent.PolicyRuleUser - StateEventContent.RoomAliases -> NotificationContent.StateEvent.RoomAliases StateEventContent.RoomAvatar -> NotificationContent.StateEvent.RoomAvatar StateEventContent.RoomCanonicalAlias -> NotificationContent.StateEvent.RoomCanonicalAlias StateEventContent.RoomCreate -> NotificationContent.StateEvent.RoomCreate diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index 644c5aefc2..e6287d0d16 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.location.LiveLocationShare import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver @@ -43,10 +44,11 @@ import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.history.map import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest -import io.element.android.libraries.matrix.impl.room.location.map import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher +import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListService import io.element.android.libraries.matrix.impl.roomdirectory.map import io.element.android.libraries.matrix.impl.timeline.RustTimeline +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.util.MessageEventContent import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver @@ -68,7 +70,6 @@ import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener import org.matrix.rustcomponents.sdk.KnockRequestsListener -import org.matrix.rustcomponents.sdk.LiveLocationShareListener import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate import org.matrix.rustcomponents.sdk.SendQueueListener @@ -147,6 +148,12 @@ class JoinedRustRoom( override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.Live) + override val threadsListService: ThreadsListService = RustThreadsListService( + inner = innerRoom.threadListService(), + contentMapper = TimelineEventContentMapper(), + roomCoroutineScope = roomCoroutineScope, + ) + override val syncUpdateFlow = flow { var counter = 0L liveTimeline.onSyncedEventReceived.collect { @@ -504,13 +511,7 @@ class JoinedRustRoom( } override fun subscribeToLiveLocationShares(): Flow> { - return mxCallbackFlow { - innerRoom.subscribeToLiveLocationShares(object : LiveLocationShareListener { - override fun call(liveLocationShares: List) { - trySend(liveLocationShares.map { it.map() }) - } - }) - } + TODO("Not implemented yet") } override suspend fun startLiveLocationShare(durationMillis: Long): Result = withContext(roomDispatcher) { @@ -536,6 +537,7 @@ class JoinedRustRoom( override fun destroy() { baseRoom.destroy() liveInnerTimeline.destroy() + threadsListService.destroy() Timber.d("Room $roomId destroyed") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt index 76fea0beef..897d9a34cb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt @@ -16,7 +16,6 @@ fun StateEventType.map(): RustStateEventType = when (this) { StateEventType.PolicyRuleServer -> RustStateEventType.PolicyRuleServer StateEventType.PolicyRuleUser -> RustStateEventType.PolicyRuleUser StateEventType.CallMember -> RustStateEventType.CallMember - StateEventType.RoomAliases -> RustStateEventType.RoomAliases StateEventType.RoomAvatar -> RustStateEventType.RoomAvatar StateEventType.RoomCanonicalAlias -> RustStateEventType.RoomCanonicalAlias StateEventType.RoomCreate -> RustStateEventType.RoomCreate @@ -46,7 +45,6 @@ fun RustStateEventType.map(): StateEventType = when (this) { RustStateEventType.PolicyRuleServer -> StateEventType.PolicyRuleServer RustStateEventType.PolicyRuleUser -> StateEventType.PolicyRuleUser RustStateEventType.CallMember -> StateEventType.CallMember - RustStateEventType.RoomAliases -> StateEventType.RoomAliases RustStateEventType.RoomAvatar -> StateEventType.RoomAvatar RustStateEventType.RoomCanonicalAlias -> StateEventType.RoomCanonicalAlias RustStateEventType.RoomCreate -> StateEventType.RoomCreate diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationShareMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationShareMapper.kt deleted file mode 100644 index 3b80c1c61f..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationShareMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.room.location - -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.location.LiveLocationShare -import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare - -fun RustLiveLocationShare.map(): LiveLocationShare { - return LiveLocationShare( - userId = UserId(userId), - lastGeoUri = lastLocation.location.geoUri, - lastTimestamp = lastLocation.ts.toLong(), - isLive = isLive, - ) -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListService.kt new file mode 100644 index 0000000000..a74c5bc378 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListService.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.threads + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem +import io.element.android.libraries.matrix.api.room.threads.ThreadListItemEvent +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService +import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper +import io.element.android.libraries.matrix.impl.timeline.item.event.map +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import org.matrix.rustcomponents.sdk.ThreadListEntriesListener +import org.matrix.rustcomponents.sdk.ThreadListPaginationStateListener +import org.matrix.rustcomponents.sdk.ThreadListUpdate +import uniffi.matrix_sdk_ui.ThreadListPaginationState +import org.matrix.rustcomponents.sdk.ThreadListService as InnerThreadListService + +class RustThreadsListService( + private val inner: InnerThreadListService, + private val roomCoroutineScope: CoroutineScope, + private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(), +) : ThreadsListService { + private var itemSubscriptionJob: Job? = null + + private val items = MutableStateFlow>(emptyList()) + + override fun subscribeToItemUpdates(): Flow> { + if (itemSubscriptionJob?.isActive != true) { + itemSubscriptionJob = doSubscribeToItemUpdates() + } + + return items + } + + private fun doSubscribeToItemUpdates(): Job { + val updatesFlow = mxCallbackFlow { + inner.subscribeToItemsUpdates(object : ThreadListEntriesListener { + override fun onUpdate(diff: List) { + trySend(diff) + } + }) + } + + return updatesFlow + .onStart { items.value = inner.items().map { it.map(contentMapper) } } + .onEach { diff -> + val updated = items.value.toMutableList() + updated.apply(diff, contentMapper) + items.value = updated + } + .launchIn(roomCoroutineScope) + } + + override fun subscribeToPaginationUpdates(): Flow { + return mxCallbackFlow { + inner.subscribeToPaginationStateUpdates(object : ThreadListPaginationStateListener { + override fun onUpdate(state: ThreadListPaginationState) { + trySend(state.map()) + } + }).also { + // Send the initial state + trySend(inner.paginationState().map()) + } + } + } + + override suspend fun paginate(): Result = runCatchingExceptions { + inner.paginate() + } + + override suspend fun reset(): Result = runCatchingExceptions { + inner.reset() + } + + override fun destroy() { + itemSubscriptionJob?.cancel() + inner.destroy() + } +} + +private fun MutableList.apply( + diff: List, + contentMapper: TimelineEventContentMapper +) { + for (diffItem in diff) { + when (diffItem) { + is ThreadListUpdate.Append -> { + val newItems = diffItem.values.map { it.map(contentMapper) } + addAll(newItems) + } + ThreadListUpdate.Clear -> clear() + is ThreadListUpdate.Insert -> { + add(diffItem.index.toInt(), diffItem.value.map(contentMapper)) + } + ThreadListUpdate.PopBack -> { + removeAt(lastIndex) + } + ThreadListUpdate.PopFront -> { + removeAt(0) + } + is ThreadListUpdate.PushBack -> { + add(diffItem.value.map(contentMapper)) + } + is ThreadListUpdate.PushFront -> { + add(0, diffItem.value.map(contentMapper)) + } + is ThreadListUpdate.Remove -> { + removeAt(diffItem.index.toInt()) + } + is ThreadListUpdate.Reset -> { + clear() + addAll(diffItem.values.map { it.map(contentMapper) }) + } + is ThreadListUpdate.Set -> { + set(diffItem.index.toInt(), diffItem.value.map(contentMapper)) + } + is ThreadListUpdate.Truncate -> { + subList(diffItem.length.toInt(), size).clear() + } + } + } +} + +fun org.matrix.rustcomponents.sdk.ThreadListItem.map(contentMapper: TimelineEventContentMapper): ThreadListItem = ThreadListItem( + rootEvent = rootEvent.map(contentMapper), + latestEvent = latestEvent?.map(contentMapper), + numberOfReplies = numReplies.toLong(), +) + +fun org.matrix.rustcomponents.sdk.ThreadListItemEvent.map(contentMapper: TimelineEventContentMapper): ThreadListItemEvent = ThreadListItemEvent( + eventId = EventId(eventId), + senderId = UserId(sender), + isOwn = isOwn, + senderProfile = senderProfile.map(), + content = content?.let(contentMapper::map), + timestamp = timestamp.toLong(), +) + +fun ThreadListPaginationState.map(): ThreadListPaginationStatus = when (this) { + is ThreadListPaginationState.Idle -> ThreadListPaginationStatus.Idle(hasMoreToLoad = !endReached) + ThreadListPaginationState.Loading -> ThreadListPaginationStatus.Loading +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index df7bd0442d..e5cc9450a9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.TimelineException @@ -129,6 +130,8 @@ class RustTimeline( Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode is Timeline.Mode.FocusedOnEvent) ) + private val loggerTag = "Timeline($mode)" + init { when (mode) { is Timeline.Mode.Live, is Timeline.Mode.FocusedOnEvent -> coroutineScope.fetchMembers() @@ -176,10 +179,11 @@ class RustTimeline( } private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) { - when (direction) { + val result = when (direction) { Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.getAndUpdate(update) Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update) } + Timber.tag(loggerTag).d("updatePaginationStatus $direction: $result") } // Use NonCancellable to avoid breaking the timeline when the coroutine is cancelled. @@ -194,12 +198,13 @@ class RustTimeline( } }.onFailure { error -> if (error is TimelineException.CannotPaginate) { - Timber.d("Can't paginate $direction on room ${joinedRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}") + Timber.tag(loggerTag).d("Can't paginate $direction on room ${joinedRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}") } else { updatePaginationStatus(direction) { it.copy(isPaginating = false) } - Timber.e(error, "Error paginating $direction on room ${joinedRoom.roomId}") + Timber.tag(loggerTag).e(error, "Error paginating $direction on room ${joinedRoom.roomId}") } }.onSuccess { hasReachedEnd -> + Timber.tag(loggerTag).d("Finished paginating $direction on room ${joinedRoom.roomId}, hasReachedEnd: $hasReachedEnd") updatePaginationStatus(direction) { it.copy(isPaginating = false, hasMoreToLoad = !hasReachedEnd) } } } @@ -263,7 +268,7 @@ class RustTimeline( try { inner.fetchMembers() } catch (exception: Exception) { - Timber.e(exception, "Error fetching members for room ${joinedRoom.roomId}") + Timber.tag(loggerTag).e(exception, "Error fetching members for room ${joinedRoom.roomId}") } } @@ -271,8 +276,16 @@ class RustTimeline( body: String, htmlBody: String?, intentionalMentions: List, + msgType: MsgType, + asPlainText: Boolean, ): Result = withContext(dispatcher) { - MessageEventContent.from(body, htmlBody, intentionalMentions).use { content -> + MessageEventContent.from( + body = body, + htmlBody = htmlBody, + intentionalMentions = intentionalMentions, + msgType = msgType, + asPlainText = asPlainText, + ).use { content -> runCatchingExceptions { inner.send(content) } @@ -354,9 +367,15 @@ class RustTimeline( htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, + msgType: MsgType, ): Result = withContext(dispatcher) { runCatchingExceptions { - val msg = MessageEventContent.from(body, htmlBody, intentionalMentions) + val msg = MessageEventContent.from( + body = body, + htmlBody = htmlBody, + intentionalMentions = intentionalMentions, + msgType = msgType, + ) inner.sendReply( msg = msg, eventId = repliedToEventId.value, @@ -372,7 +391,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending image ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending image ${file.path.hash()}") return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendImage( params = UploadParameters( @@ -398,7 +417,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending video ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending video ${file.path.hash()}") return sendAttachment(listOfNotNull(file, thumbnailFile)) { inner.sendVideo( params = UploadParameters( @@ -423,7 +442,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending audio ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending audio ${file.path.hash()}") return sendAttachment(listOf(file)) { inner.sendAudio( params = UploadParameters( @@ -447,7 +466,7 @@ class RustTimeline( formattedCaption: String?, inReplyToEventId: EventId?, ): Result { - Timber.d("Sending file ${file.path.hash()}") + Timber.tag(loggerTag).d("Sending file ${file.path.hash()}") return sendAttachment(listOf(file)) { inner.sendFile( params = UploadParameters( @@ -477,7 +496,7 @@ class RustTimeline( runCatchingExceptions { roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) }.onFailure { - Timber.e(it) + Timber.tag(loggerTag).e(it) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index 829dc3991d..763a3c4184 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -232,7 +232,6 @@ private fun RustOtherState.map(): OtherState { RustOtherState.PolicyRuleRoom -> OtherState.PolicyRuleRoom RustOtherState.PolicyRuleServer -> OtherState.PolicyRuleServer RustOtherState.PolicyRuleUser -> OtherState.PolicyRuleUser - RustOtherState.RoomAliases -> OtherState.RoomAliases is RustOtherState.RoomAvatar -> OtherState.RoomAvatar(url) RustOtherState.RoomCanonicalAlias -> OtherState.RoomCanonicalAlias RustOtherState.RoomCreate -> OtherState.RoomCreate diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/MessageEventContent.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/MessageEventContent.kt index 3e320116c6..f1c0019f17 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/MessageEventContent.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/MessageEventContent.kt @@ -9,20 +9,54 @@ package io.element.android.libraries.matrix.impl.util import io.element.android.libraries.matrix.api.room.IntentionalMention +import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.impl.room.map +import org.matrix.rustcomponents.sdk.MessageContent +import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation +import org.matrix.rustcomponents.sdk.TextMessageContent +import org.matrix.rustcomponents.sdk.contentWithoutRelationFromMessage import org.matrix.rustcomponents.sdk.messageEventContentFromHtml +import org.matrix.rustcomponents.sdk.messageEventContentFromHtmlAsEmote import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdownAsEmote /** * Creates a [RoomMessageEventContentWithoutRelation] from a body, an html body and a list of mentions. */ object MessageEventContent { - fun from(body: String, htmlBody: String?, intentionalMentions: List): RoomMessageEventContentWithoutRelation { - return if (htmlBody != null) { - messageEventContentFromHtml(body, htmlBody) - } else { - messageEventContentFromMarkdown(body) - }.withMentions(intentionalMentions.map()) + fun from( + body: String, + htmlBody: String?, + intentionalMentions: List, + msgType: MsgType = MsgType.MSG_TYPE_TEXT, + asPlainText: Boolean = false, + ): RoomMessageEventContentWithoutRelation { + return when { + asPlainText -> contentWithoutRelationFromMessage( + MessageContent( + msgType = MessageType.Text( + TextMessageContent( + body = body, + formatted = null, + ) + ), + body = body, + isEdited = false, + mentions = null, + ) + ) + htmlBody != null -> if (msgType == MsgType.MSG_TYPE_EMOTE) { + messageEventContentFromHtmlAsEmote(body, htmlBody) + } else { + messageEventContentFromHtml(body, htmlBody) + } + else -> if (msgType == MsgType.MSG_TYPE_EMOTE) { + messageEventContentFromMarkdownAsEmote(body) + } else { + messageEventContentFromMarkdown(body) + } + } + .withMentions(intentionalMentions.map()) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProviderTest.kt new file mode 100644 index 0000000000..8d3377d698 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustHomeserverCapabilitiesProviderTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverCapabilities +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RustHomeserverCapabilitiesProviderTest { + @Test + fun `refresh calls client refresh`() = runTest { + val refreshLambda = lambdaRecorder {} + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(refresh = refreshLambda), + ) + assertThat(provider.refresh().isSuccess).isTrue() + refreshLambda.assertions().isCalledOnce() + } + + @Test + fun `refresh fails when client refresh does`() = runTest { + val refreshLambda = lambdaRecorder { throw IllegalStateException("Failed to refresh capabilities") } + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(refresh = refreshLambda), + ) + assertThat(provider.refresh().isFailure).isTrue() + refreshLambda.assertions().isCalledOnce() + } + + @Test + fun `canChangeDisplayName returns expected value`() = runTest { + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(canChangeDisplayName = { true }), + ) + assertThat(provider.canChangeDisplayName().getOrNull()).isTrue() + } + + @Test + fun `canChangeAvatarUrl returns expected value`() = runTest { + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(canChangeAvatar = { true }), + ) + assertThat(provider.canChangeAvatarUrl().getOrNull()).isTrue() + } + + @Test + fun `canChangeDisplayName returns failure when client throws`() = runTest { + val provider = createCapabilitiesProvider( + capabilities = FakeFfiHomeserverCapabilities(canChangeDisplayName = { throw IllegalStateException("Failed to get display name capability") }), + ) + assert(provider.canChangeDisplayName().isFailure) + } + + private fun createCapabilitiesProvider( + capabilities: FakeFfiHomeserverCapabilities = FakeFfiHomeserverCapabilities(), + ) = RustHomeserverCapabilitiesProvider(capabilities) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt index 41823a0fbb..8689de3c9b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt @@ -14,10 +14,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID import org.matrix.rustcomponents.sdk.EventOrTransactionId import org.matrix.rustcomponents.sdk.EventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem -import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo +import org.matrix.rustcomponents.sdk.LazyTimelineItemProvider import org.matrix.rustcomponents.sdk.ProfileDetails import org.matrix.rustcomponents.sdk.Receipt -import org.matrix.rustcomponents.sdk.ShieldState import org.matrix.rustcomponents.sdk.TimelineItemContent import uniffi.matrix_sdk_ui.EventItemOrigin @@ -26,37 +25,35 @@ internal fun aRustEventTimelineItem( eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value), sender: String = A_USER_ID.value, senderProfile: ProfileDetails = ProfileDetails.Unavailable, + forwarder: String? = null, + forwarderProfile: ProfileDetails? = null, isOwn: Boolean = true, isEditable: Boolean = true, content: TimelineItemContent = aRustTimelineItemContentMsgLike(), + eventTypeRaw: String? = null, timestamp: ULong = 0uL, - debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(), localSendState: EventSendState? = null, + localCreatedAt: ULong? = null, readReceipts: Map = emptyMap(), origin: EventItemOrigin? = EventItemOrigin.SYNC, canBeRepliedTo: Boolean = true, - shieldsState: ShieldState = ShieldState.None, - localCreatedAt: ULong? = null, - forwarder: String? = null, - forwarderProfile: ProfileDetails? = null, + lazyProvider: LazyTimelineItemProvider = FakeFfiLazyTimelineItemProvider(), ) = EventTimelineItem( isRemote = isRemote, eventOrTransactionId = eventOrTransactionId, sender = sender, senderProfile = senderProfile, - timestamp = timestamp, - isOwn = isOwn, - isEditable = isEditable, - canBeRepliedTo = canBeRepliedTo, - content = content, - localSendState = localSendState, - readReceipts = readReceipts, - origin = origin, - localCreatedAt = localCreatedAt, - lazyProvider = FakeFfiLazyTimelineItemProvider( - debugInfo = debugInfo, - shieldsState = shieldsState, - ), forwarder = forwarder, forwarderProfile = forwarderProfile, + isOwn = isOwn, + isEditable = isEditable, + content = content, + eventTypeRaw = eventTypeRaw, + timestamp = timestamp, + localSendState = localSendState, + localCreatedAt = localCreatedAt, + readReceipts = readReceipts, + origin = origin, + canBeRepliedTo = canBeRepliedTo, + lazyProvider = lazyProvider, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 2aec38fcde..57ffcddb37 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.CreateRoomParameters import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.HomeserverCapabilities import org.matrix.rustcomponents.sdk.HomeserverLoginDetails import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NoHandle @@ -50,6 +51,7 @@ class FakeFfiClient( private val homeserverLoginDetailsResult: () -> HomeserverLoginDetails = { lambdaError() }, private val getStoreSizesResult: () -> StoreSizes = { lambdaError() }, private val createRoomResult: (CreateRoomParameters) -> String = { lambdaError() }, + private val homeserverCapabilities: HomeserverCapabilities = FakeFfiHomeserverCapabilities(), private val closeResult: () -> Unit = {}, ) : Client(NoHandle) { override fun userId(): String = userId @@ -103,5 +105,9 @@ class FakeFfiClient( return createRoomResult(request) } + override fun homeserverCapabilities(): HomeserverCapabilities { + return homeserverCapabilities + } + override fun close() = closeResult() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverCapabilities.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverCapabilities.kt new file mode 100644 index 0000000000..4c60cbbb49 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverCapabilities.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.lambda.lambdaError +import org.matrix.rustcomponents.sdk.ExtendedProfileFields +import org.matrix.rustcomponents.sdk.HomeserverCapabilities +import org.matrix.rustcomponents.sdk.NoHandle + +class FakeFfiHomeserverCapabilities( + private val refresh: () -> Unit = { lambdaError() }, + private val canChangeDisplayName: () -> Boolean = { lambdaError() }, + private val canChangeAvatar: () -> Boolean = { lambdaError() }, + private val canChangePassword: () -> Boolean = { lambdaError() }, + private val canChangeThirdPartyIds: () -> Boolean = { lambdaError() }, + private val canGetLoginToken: () -> Boolean = { lambdaError() }, + private val forgetsRoomWhenLeaving: () -> Boolean = { lambdaError() }, + private val extendedProfileFields: () -> ExtendedProfileFields = { lambdaError() }, +) : HomeserverCapabilities(NoHandle) { + override suspend fun refresh() = refresh.invoke() + override suspend fun canChangeDisplayname(): Boolean = canChangeDisplayName.invoke() + override suspend fun canChangeAvatar(): Boolean = canChangeAvatar.invoke() + override suspend fun canChangePassword(): Boolean = canChangePassword.invoke() + override suspend fun canChangeThirdpartyIds(): Boolean = canChangeThirdPartyIds.invoke() + override suspend fun canGetLoginToken(): Boolean = canGetLoginToken.invoke() + override suspend fun forgetsRoomWhenLeaving(): Boolean = forgetsRoomWhenLeaving.invoke() + override suspend fun extendedProfileFields(): ExtendedProfileFields = extendedProfileFields.invoke() +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiThreadListService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiThreadListService.kt new file mode 100644 index 0000000000..009e6a3348 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiThreadListService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoHandle +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.ThreadListEntriesListener +import org.matrix.rustcomponents.sdk.ThreadListItem +import org.matrix.rustcomponents.sdk.ThreadListPaginationStateListener +import org.matrix.rustcomponents.sdk.ThreadListService +import org.matrix.rustcomponents.sdk.ThreadListUpdate +import uniffi.matrix_sdk_ui.ThreadListPaginationState + +class FakeFfiThreadListService( + private val subscribeToItemsUpdates: (ThreadListEntriesListener) -> TaskHandle = { FakeFfiTaskHandle() }, + private val subscribeToPaginationStateUpdates: (ThreadListPaginationStateListener) -> TaskHandle = { FakeFfiTaskHandle() }, + private val items: () -> List = { emptyList() }, + private val paginationState: () -> ThreadListPaginationState = { ThreadListPaginationState.Idle(endReached = false) }, + private val paginate: suspend () -> Unit = {}, + private val reset: suspend () -> Unit = {}, + private val destroy: () -> Unit = {}, +) : ThreadListService(NoHandle) { + private var itemsListener: ThreadListEntriesListener? = null + private var paginationStateListener: ThreadListPaginationStateListener? = null + + override fun subscribeToItemsUpdates(listener: ThreadListEntriesListener): TaskHandle { + itemsListener = listener + return subscribeToItemsUpdates.invoke(listener) + } + + override fun subscribeToPaginationStateUpdates(listener: ThreadListPaginationStateListener): TaskHandle { + paginationStateListener = listener + return subscribeToPaginationStateUpdates.invoke(listener) + } + + override fun items(): List = items.invoke() + + override fun paginationState(): ThreadListPaginationState = paginationState.invoke() + + override suspend fun paginate() = paginate.invoke() + + override suspend fun reset() = reset.invoke() + + override fun destroy() = destroy.invoke() + + fun emitUpdates(updates: List) { + itemsListener?.onUpdate(updates) + } + + fun emitPaginationState(state: ThreadListPaginationState) { + paginationStateListener?.onUpdate(state) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt index 428bb7db7a..93a8f0908f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/StateEventTypeTest.kt @@ -20,7 +20,6 @@ class StateEventTypeTest { assertThat(RustStateEventType.PolicyRuleRoom.map()).isEqualTo(StateEventType.PolicyRuleRoom) assertThat(RustStateEventType.PolicyRuleServer.map()).isEqualTo(StateEventType.PolicyRuleServer) assertThat(RustStateEventType.PolicyRuleUser.map()).isEqualTo(StateEventType.PolicyRuleUser) - assertThat(RustStateEventType.RoomAliases.map()).isEqualTo(StateEventType.RoomAliases) assertThat(RustStateEventType.RoomAvatar.map()).isEqualTo(StateEventType.RoomAvatar) assertThat(RustStateEventType.RoomCanonicalAlias.map()).isEqualTo(StateEventType.RoomCanonicalAlias) assertThat(RustStateEventType.RoomCreate.map()).isEqualTo(StateEventType.RoomCreate) @@ -47,7 +46,6 @@ class StateEventTypeTest { assertThat(StateEventType.PolicyRuleRoom.map()).isEqualTo(RustStateEventType.PolicyRuleRoom) assertThat(StateEventType.PolicyRuleServer.map()).isEqualTo(RustStateEventType.PolicyRuleServer) assertThat(StateEventType.PolicyRuleUser.map()).isEqualTo(RustStateEventType.PolicyRuleUser) - assertThat(StateEventType.RoomAliases.map()).isEqualTo(RustStateEventType.RoomAliases) assertThat(StateEventType.RoomAvatar.map()).isEqualTo(RustStateEventType.RoomAvatar) assertThat(StateEventType.RoomCanonicalAlias.map()).isEqualTo(RustStateEventType.RoomCanonicalAlias) assertThat(StateEventType.RoomCreate.map()).isEqualTo(RustStateEventType.RoomCreate) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListServiceTest.kt new file mode 100644 index 0000000000..0fc9ad0603 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/threads/RustThreadsListServiceTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.room.threads + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustTimelineItemContentMsgLike +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTaskHandle +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiThreadListService +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_TIMESTAMP +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.ProfileDetails +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.ThreadListEntriesListener +import org.matrix.rustcomponents.sdk.ThreadListItem +import org.matrix.rustcomponents.sdk.ThreadListItemEvent +import org.matrix.rustcomponents.sdk.ThreadListPaginationStateListener +import org.matrix.rustcomponents.sdk.ThreadListUpdate +import uniffi.matrix_sdk_ui.ThreadListPaginationState + +@OptIn(ExperimentalCoroutinesApi::class) +class RustThreadsListServiceTest { + @Test + fun `subscribing to item updates calls the FFI method and allows retrieving new items`() = runTest { + val subscribeToItemsUpdatesRecorder = lambdaRecorder { FakeFfiTaskHandle() } + val inner = FakeFfiThreadListService(subscribeToItemsUpdates = subscribeToItemsUpdatesRecorder) + val service = createThreadsListService(inner = inner) + + service.subscribeToItemUpdates().test { + assertThat(awaitItem()).isEmpty() + + runCurrent() + subscribeToItemsUpdatesRecorder.assertions().isCalledOnce() + + inner.emitUpdates(listOf(aRustThreadListUpdate())) + + assertThat(awaitItem()).isNotEmpty() + } + } + + @Suppress("UnusedFlow") + @Test + fun `subscribing to item updates twice only calls the FFI method once`() = runTest { + val subscribeToItemsUpdatesRecorder = lambdaRecorder { FakeFfiTaskHandle() } + val inner = FakeFfiThreadListService(subscribeToItemsUpdates = subscribeToItemsUpdatesRecorder) + val service = createThreadsListService(inner = inner) + + service.subscribeToItemUpdates() + service.subscribeToItemUpdates() + + runCurrent() + + subscribeToItemsUpdatesRecorder.assertions().isCalledOnce() + } + + @Test + fun `subscribing to pagination updates calls the FFI method and allows retrieving new items`() = runTest { + val subscribeToPaginationUpdatesRecorder = lambdaRecorder { FakeFfiTaskHandle() } + val inner = FakeFfiThreadListService(subscribeToPaginationStateUpdates = subscribeToPaginationUpdatesRecorder) + val service = createThreadsListService(inner = inner) + + service.subscribeToPaginationUpdates().test { + assertThat(awaitItem()).isEqualTo(ThreadListPaginationStatus.Idle(hasMoreToLoad = true)) + + runCurrent() + subscribeToPaginationUpdatesRecorder.assertions().isCalledOnce() + + inner.emitPaginationState(ThreadListPaginationState.Loading) + + assertThat(awaitItem()).isEqualTo(ThreadListPaginationStatus.Loading) + } + } + + @Test + fun `paginate calls the FFI method`() = runTest { + val paginateRecorder = lambdaRecorder {} + val inner = FakeFfiThreadListService(paginate = paginateRecorder) + val service = createThreadsListService(inner = inner) + + service.paginate() + + paginateRecorder.assertions().isCalledOnce() + } + + @Test + fun `reset calls the FFI method`() = runTest { + val resetRecorder = lambdaRecorder {} + val inner = FakeFfiThreadListService(reset = resetRecorder) + val service = createThreadsListService(inner = inner) + + service.reset() + + resetRecorder.assertions().isCalledOnce() + } + + @Test + fun `destroy calls the FFI method`() = runTest { + val destroyRecorder = lambdaRecorder {} + val inner = FakeFfiThreadListService(destroy = destroyRecorder) + val service = createThreadsListService(inner = inner) + + service.destroy() + + destroyRecorder.assertions().isCalledOnce() + } + + private fun TestScope.createThreadsListService( + inner: FakeFfiThreadListService = FakeFfiThreadListService(), + ) = RustThreadsListService( + inner = inner, + roomCoroutineScope = backgroundScope, + ) + + private fun aRustThreadListUpdate() = ThreadListUpdate.Append( + values = listOf( + ThreadListItem( + rootEvent = ThreadListItemEvent( + eventId = AN_EVENT_ID.value, + timestamp = A_TIMESTAMP.toULong(), + sender = A_USER_ID.value, + senderProfile = ProfileDetails.Pending, + isOwn = true, + content = aRustTimelineItemContentMsgLike(), + ), + numReplies = 0u, + latestEvent = null, + ) + ), + ) +} diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index ccb1a37a25..63836d857a 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.coroutines.test) - implementation(projects.libraries.matrix.impl) implementation(projects.services.analytics.api) implementation(projects.tests.testutils) implementation(libs.kotlinx.collections.immutable) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeHomeserverCapabilitiesProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeHomeserverCapabilitiesProvider.kt new file mode 100644 index 0000000000..c098388c89 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeHomeserverCapabilitiesProvider.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test + +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider + +class FakeHomeserverCapabilitiesProvider( + private val refresh: () -> Result = { Result.success(Unit) }, + private val canChangeDisplayName: () -> Result = { Result.success(true) }, + private val canChangeAvatarUrl: () -> Result = { Result.success(true) }, +) : HomeserverCapabilitiesProvider { + override suspend fun refresh(): Result = refresh.invoke() + override suspend fun canChangeDisplayName(): Result = canChangeDisplayName.invoke() + override suspend fun canChangeAvatarUrl(): Result = canChangeAvatarUrl.invoke() +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 56527574d7..742af160ae 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.test +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes import io.element.android.libraries.matrix.api.core.DeviceId @@ -84,6 +85,7 @@ class FakeMatrixClient( override val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), + private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(), private val accountManagementUrlResult: (AccountManagementAction?) -> Result = { lambdaError() }, private val resolveRoomAliasResult: (RoomAlias) -> Result> = { Result.success( @@ -384,4 +386,8 @@ class FakeMatrixClient( override suspend fun resetWellKnownConfig(): Result { return resetWellKnownConfigLambda() } + + override fun homeserverCapabilities(): HomeserverCapabilitiesProvider { + return homeserverCapabilitiesProvider + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt index c4acccb55c..238ad2663d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.test.auth import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.ElementClassicSession import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -17,6 +18,7 @@ import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -32,6 +34,8 @@ class FakeMatrixAuthenticationService( lambdaRecorder Unit, Result> { _, _ -> Result.success(A_SESSION_ID) }, private val importCreatedSessionLambda: (ExternalSession) -> Result = { lambdaError() }, private val setHomeserverResult: (String) -> Result = { lambdaError() }, + private val setElementClassicSessionResult: (ElementClassicSession?) -> Unit = { lambdaError() }, + private val doSecretsContainBackupKeyResult: (UserId, String, String) -> Boolean = { _, _, _ -> lambdaError() }, ) : MatrixAuthenticationService { private var oidcError: Throwable? = null private var oidcCancelError: Throwable? = null @@ -108,4 +112,12 @@ class FakeMatrixAuthenticationService( fun givenMatrixClient(matrixClient: MatrixClient) { this.matrixClient = matrixClient } + + override fun setElementClassicSession(session: ElementClassicSession?) { + setElementClassicSessionResult(session) + } + + override fun doSecretsContainBackupKey(userId: UserId, secrets: String, backupInfo: String): Boolean { + return doSecretsContainBackupKeyResult(userId, secrets, backupInfo) + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt index c348cd351c..d49a1cc22d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/mxc/FakeMxcTools.kt @@ -8,8 +8,12 @@ package io.element.android.libraries.matrix.test.mxc import io.element.android.libraries.matrix.api.mxc.MxcTools -import io.element.android.libraries.matrix.impl.mxc.DefaultMxcTools +import io.element.android.tests.testutils.lambda.lambdaError class FakeMxcTools( - private val delegate: MxcTools = DefaultMxcTools() -) : MxcTools by delegate + private val mxcUri2FilePathResult: (String) -> String? = { lambdaError() } +) : MxcTools { + override fun mxcUri2FilePath(mxcUri: String): String? { + return mxcUri2FilePathResult(mxcUri) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt index a4580334e4..84497b38de 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeJoinedRoom.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask @@ -56,6 +57,7 @@ class FakeJoinedRoom( override val roomNotificationSettingsStateFlow: StateFlow = MutableStateFlow(RoomNotificationSettingsState.Unknown), override val knockRequestsFlow: Flow> = MutableStateFlow(emptyList()), + override val threadsListService: FakeThreadsListService = FakeThreadsListService(), private val roomNotificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private var createTimelineResult: (CreateTimelineParams) -> Result = { lambdaError() }, private val editMessageLambda: (EventId, String, String?, List) -> Result = { _, _, _, _ -> lambdaError() }, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/threads/FakeThreadsListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/threads/FakeThreadsListService.kt new file mode 100644 index 0000000000..a1e719ffb2 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/threads/FakeThreadsListService.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.room.threads + +import io.element.android.libraries.matrix.api.room.threads.ThreadListItem +import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus +import io.element.android.libraries.matrix.api.room.threads.ThreadsListService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeThreadsListService( + private val items: MutableStateFlow> = MutableStateFlow(emptyList()), + private val paginationStatus: MutableStateFlow = MutableStateFlow(ThreadListPaginationStatus.Idle(hasMoreToLoad = true)), + private val subscribeToItemUpdates: () -> Flow> = { items }, + private val subscribeToPaginationUpdates: () -> Flow = { paginationStatus }, + private val paginate: suspend () -> Result = { Result.success(Unit) }, + private val reset: suspend () -> Result = { Result.success(Unit) }, + private val destroy: () -> Unit = {}, +) : ThreadsListService { + override fun subscribeToItemUpdates(): Flow> { + return subscribeToItemUpdates.invoke() + } + + override fun subscribeToPaginationUpdates(): Flow { + return subscribeToPaginationUpdates.invoke() + } + + override suspend fun paginate(): Result { + return paginate.invoke() + } + + override suspend fun reset(): Result { + return reset.invoke() + } + + override fun destroy() { + return destroy.invoke() + } + + suspend fun emit(items: List) { + this.items.emit(items) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index fbe837ba2c..302013044e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -78,7 +79,9 @@ class FakeTimeline( body: String, htmlBody: String?, intentionalMentions: List, - ) -> Result = { _, _, _ -> + msgType: MsgType, + asPlainText: Boolean, + ) -> Result = { _, _, _, _, _ -> lambdaError() } @@ -90,8 +93,10 @@ class FakeTimeline( body: String, htmlBody: String?, intentionalMentions: List, + msgType: MsgType, + asPlainText: Boolean, ): Result = simulateLongTask { - sendMessageLambda(body, htmlBody, intentionalMentions) + sendMessageLambda(body, htmlBody, intentionalMentions, msgType, asPlainText) } var redactEventLambda: (eventOrTransactionId: EventOrTransactionId, reason: String?) -> Result = { _, _ -> @@ -148,7 +153,8 @@ class FakeTimeline( htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, - ) -> Result = { _, _, _, _, _ -> + msgType: MsgType, + ) -> Result = { _, _, _, _, _, _ -> lambdaError() } @@ -158,12 +164,14 @@ class FakeTimeline( htmlBody: String?, intentionalMentions: List, fromNotification: Boolean, + msgType: MsgType, ): Result = replyMessageLambda( repliedToEventId, body, htmlBody, intentionalMentions, fromNotification, + msgType, ) var sendImageLambda: ( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt index dca173d780..7e0e51050a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.ui.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -22,9 +23,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType @@ -33,6 +38,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.R @@ -48,10 +54,23 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun CreateDmConfirmationBottomSheet( matrixUser: MatrixUser, + enableKeyShareOnInvite: Boolean, + isUserIdentityUnknown: Boolean, onSendInvite: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { + val titleContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { + stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_title) + } else { + stringResource(R.string.screen_bottom_sheet_create_dm_title) + } + val descriptionContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { + stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_content) + } else { + stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()) + } + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismiss, @@ -63,47 +82,95 @@ fun CreateDmConfirmationBottomSheet( .padding(top = 24.dp, bottom = 16.dp, start = 16.dp, end = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Avatar( - avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation), - avatarType = AvatarType.User, - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(R.string.screen_bottom_sheet_create_dm_title), - style = ElementTheme.typography.fontHeadingMdBold, - color = ElementTheme.colors.textPrimary, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()), - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(40.dp)) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = onSendInvite, - leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()), - text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title), - ) - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - modifier = Modifier.fillMaxWidth(), - onClick = onDismiss, - text = stringResource(CommonStrings.action_cancel), - ) + if (isUserIdentityUnknown) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding( + bottom = 16.dp, + start = 16.dp, + end = 16.dp, + ), + title = titleContent, + subTitle = descriptionContent, + iconStyle = BigIcon.Style.Default(CompoundIcons.UserAddSolid()), + ) + MatrixUserRow(matrixUser) + Spacer(modifier = Modifier.height(32.dp)) + ButtonRowMolecule( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + text = stringResource(CommonStrings.action_cancel), + onClick = onDismiss + ) + Button( + modifier = Modifier.weight(1f), + text = stringResource(CommonStrings.action_continue), + onClick = onSendInvite + ) + } + Spacer(modifier = Modifier.height(32.dp)) + } else { + Avatar( + avatarData = matrixUser.getAvatarData(AvatarSize.DmCreationConfirmation), + avatarType = AvatarType.User, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = titleContent, + style = ElementTheme.typography.fontHeadingMdBold, + color = ElementTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = descriptionContent, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(40.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onSendInvite, + leadingIcon = IconSource.Vector(CompoundIcons.UserAdd()), + text = stringResource(R.string.screen_bottom_sheet_create_dm_confirmation_button_title), + ) + Spacer(modifier = Modifier.height(16.dp)) + TextButton( + modifier = Modifier.fillMaxWidth(), + onClick = onDismiss, + text = stringResource(CommonStrings.action_cancel), + ) + } } } } @PreviewsDayNight @Composable -internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { +internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter( + CreateDmConfirmationBottomSheetStateProvider::class +) state: CreateDmConfirmationBottomSheetState) = ElementPreview { CreateDmConfirmationBottomSheet( - matrixUser = matrixUser, + matrixUser = state.matrixUser, + enableKeyShareOnInvite = state.enableKeyShareOnInvite, + isUserIdentityUnknown = state.isUserIdentityUnknown, onSendInvite = {}, onDismiss = {}, ) } + +data class CreateDmConfirmationBottomSheetState( + val matrixUser: MatrixUser, + val enableKeyShareOnInvite: Boolean, + val isUserIdentityUnknown: Boolean, +) + +class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider { + override val values = sequenceOf( + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = false, isUserIdentityUnknown = false), + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = false), + CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = true), + ) +} diff --git a/libraries/matrixui/src/main/res/values-ja/translations.xml b/libraries/matrixui/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..ce2b24041c --- /dev/null +++ b/libraries/matrixui/src/main/res/values-ja/translations.xml @@ -0,0 +1,9 @@ + + + "招待を送信" + "%1$s とチャットを始めますか?" + "招待を送信しますか?" + "この人物とのチャットがありません。続行する前に、まず招待してください。" + "この新しい連絡先と新規にチャットを開始しますか?" + "%1$s (%2$s) があなたを招待しました" + diff --git a/libraries/matrixui/src/main/res/values-vi/translations.xml b/libraries/matrixui/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..638996cb06 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-vi/translations.xml @@ -0,0 +1,4 @@ + + + "%1$s(%2$s ) đã mời bạn" + diff --git a/libraries/matrixui/src/main/res/values/localazy.xml b/libraries/matrixui/src/main/res/values/localazy.xml index b27021ebd6..c4000519a5 100644 --- a/libraries/matrixui/src/main/res/values/localazy.xml +++ b/libraries/matrixui/src/main/res/values/localazy.xml @@ -3,5 +3,7 @@ "Send invite" "Would you like to start a chat with %1$s?" "Send invite?" + "You currently don’t have any chats with this person. Confirm inviting them before continuing." + "Start a chat with this new contact?" "%1$s (%2$s) invited you" diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index 2b1883619e..2ab1cd0619 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -195,18 +195,16 @@ class AndroidMediaPreProcessor( file = file, mimeType = mimeType, ) - val imageInfo = contentResolver.openInputStream(uri).use { input -> - val bitmap = BitmapFactory.decodeStream(input, null, null)!! - ImageInfo( - width = bitmap.width.toLong(), - height = bitmap.height.toLong(), - mimetype = mimeType, - size = file.length(), - thumbnailInfo = thumbnailResult?.info, - thumbnailSource = null, - blurhash = thumbnailResult?.blurhash, - ) - } + val (width, height) = extractOrientedImageDimensions(file) + val imageInfo = ImageInfo( + width = width, + height = height, + mimetype = mimeType, + size = file.length(), + thumbnailInfo = thumbnailResult?.info, + thumbnailSource = null, + blurhash = thumbnailResult?.blurhash, + ) removeSensitiveImageMetadata(file) return MediaUploadInfo.Image( file = file, @@ -354,6 +352,23 @@ class AndroidMediaPreProcessor( return contentResolver.openInputStream(uri)?.use { createTmpFileWithInput(it) } ?: error("Could not copy the contents of $uri to a temporary file") } + + private fun extractOrientedImageDimensions(file: File): Pair { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.path, options) + + val rawWidth = options.outWidth.toLong() + val rawHeight = options.outHeight.toLong() + val orientation = tryOrNull { + ExifInterface(file).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } ?: ExifInterface.ORIENTATION_UNDEFINED + + return orientedImageDimensions( + rawWidth = rawWidth, + rawHeight = rawHeight, + orientation = orientation, + ) + } } private fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailResult: ThumbnailResult?) = ImageInfo( @@ -371,3 +386,18 @@ private fun MediaMetadataRetriever.extractDuration(): Duration { val durationInMs = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L return durationInMs.milliseconds } + +internal fun orientedImageDimensions(rawWidth: Long, rawHeight: Long, orientation: Int): Pair { + return if (orientation.rotatesRightAngle()) { + rawHeight to rawWidth + } else { + rawWidth to rawHeight + } +} + +private fun Int.rotatesRightAngle(): Boolean { + return this == ExifInterface.ORIENTATION_ROTATE_90 || + this == ExifInterface.ORIENTATION_ROTATE_270 || + this == ExifInterface.ORIENTATION_TRANSPOSE || + this == ExifInterface.ORIENTATION_TRANSVERSE +} diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index f4b4e7d4a5..57726ac5a2 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -12,6 +12,7 @@ import android.content.Context import android.net.Uri import android.os.Build import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.file.TemporaryUriDeleter @@ -42,6 +43,30 @@ import kotlin.time.Duration @RunWith(RobolectricTestRunner::class) class AndroidMediaPreProcessorTest { + @Test + fun `orientedImageDimensions swaps width and height for 90 degree exif orientation`() { + val (width, height) = orientedImageDimensions( + rawWidth = 4032, + rawHeight = 2268, + orientation = ExifInterface.ORIENTATION_ROTATE_90, + ) + + assertThat(width).isEqualTo(2268) + assertThat(height).isEqualTo(4032) + } + + @Test + fun `orientedImageDimensions keeps width and height for upright exif orientation`() { + val (width, height) = orientedImageDimensions( + rawWidth = 4032, + rawHeight = 2268, + orientation = ExifInterface.ORIENTATION_NORMAL, + ) + + assertThat(width).isEqualTo(4032) + assertThat(height).isEqualTo(2268) + } + private suspend fun TestScope.process( asset: Asset, mediaOptimizationConfig: MediaOptimizationConfig, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt index 01115b7c91..cb9d6ae9c8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -51,6 +52,7 @@ import androidx.media3.common.Timeline import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView +import com.bumble.appyx.core.node.LocalNodeTargetVisibility import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.audio.api.AudioFocus @@ -130,6 +132,8 @@ private fun ExoPlayerMediaAudioView( mutableStateOf(null) } + val isTargetVisible = LocalNodeTargetVisibility.current + val playableState: PlayableState.Playable by remember { derivedStateOf { PlayableState.Playable( @@ -196,13 +200,21 @@ private fun ExoPlayerMediaAudioView( exoPlayer.pause() } } + LaunchedEffect(isTargetVisible) { + if (!isTargetVisible) { + exoPlayer.pause() + } + } if (localMedia?.uri != null) { LaunchedEffect(localMedia.uri) { val mediaItem = MediaItem.fromUri(localMedia.uri) exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() } } else { - exoPlayer.setMediaItems(emptyList()) + LaunchedEffect(Unit) { + exoPlayer.setMediaItems(emptyList()) + } } val context = LocalContext.current val waveform = info?.waveform @@ -247,7 +259,7 @@ private fun ExoPlayerMediaAudioView( } }, update = { playerView -> - playerView.isVisible = metadata.hasArtwork() + playerView.isVisible = metadata.hasArtwork() && isTargetVisible }, onRelease = { playerView -> playerView.player = null @@ -317,16 +329,19 @@ private fun ExoPlayerMediaAudioView( ) } - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_CREATE -> exoPlayer.addListener(playerListener) - Lifecycle.Event.ON_RESUME -> exoPlayer.prepare() - Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() - Lifecycle.Event.ON_DESTROY -> { - exoPlayer.release() + DisposableEffect(exoPlayer) { + exoPlayer.addListener(playerListener) + onDispose { + if (!exoPlayer.isReleased) { exoPlayer.removeListener(playerListener) + exoPlayer.release() } - else -> Unit + } + } + + OnLifecycleEvent { _, event -> + if (event == Lifecycle.Event.ON_PAUSE) { + exoPlayer.pause() } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt index 306a18b4d6..4c73dd215d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState import io.element.android.libraries.ui.strings.CommonStrings -import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage +import me.saket.telephoto.zoomable.coil3.ZoomableAsyncImage import me.saket.telephoto.zoomable.rememberZoomableImageState @Composable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt index 85aecc41a8..b1a2bde350 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -29,6 +29,14 @@ import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirm import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState import kotlinx.collections.immutable.toImmutableList +private const val LONG_CAPTION = "This is a very long caption that should be scrollable in the media viewer. " + + "It contains multiple lines of text to demonstrate the scrolling behavior. " + + "Line 1: Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Line 2: Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Line 3: Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. " + + "Line 4: Duis aute irure dolor in reprehenderit in voluptate velit esse cillum. " + + "Line 5: Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia." + open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( @@ -170,6 +178,22 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) ) ), + anImageMediaInfo( + senderName = "Alice", + dateSent = "21 NOV, 2024", + caption = LONG_CAPTION, + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, it) + ), + mediaInfo = it, + ) + ) + ) + }, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index 7439909330..95ce7c631f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -17,18 +17,24 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -39,14 +45,17 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -69,6 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton 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.designsystem.utils.hasCompactHeightWindowSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.media.MediaSource @@ -102,8 +112,9 @@ fun MediaViewerView( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) var showOverlay by remember { mutableStateOf(true) } - val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0 val currentData = state.listData.getOrNull(state.currentIndex) + val defaultBottomPaddingInPixels = if (LocalInspectionMode.current && !hasCompactHeightWindowSize()) 303 else 0 + BackHandler { onBackClick() } Scaffold( modifier, @@ -153,10 +164,11 @@ fun MediaViewerView( // So we need to update this value only when the `settledPage` value changes. It seems like a bug that needs to be fixed in Compose. page == pagerState.settledPage } + val navigationBarPadding = WindowInsets.navigationBars.getBottom(LocalDensity.current) MediaViewerPage( isDisplayed = isDisplayed, showOverlay = showOverlay, - bottomPaddingInPixels = bottomPaddingInPixels, + bottomPaddingInPixels = (bottomPaddingInPixels - navigationBarPadding).coerceAtLeast(0), data = dataForPage, textFileViewer = textFileViewer, onDismiss = onBackClick, @@ -175,9 +187,7 @@ fun MediaViewerView( // Bottom bar AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { Box( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + modifier = Modifier.fillMaxSize() ) { MediaViewerBottomBar( modifier = Modifier.align(Alignment.BottomCenter), @@ -538,19 +548,46 @@ private fun MediaViewerBottomBar( if (showDivider) { HorizontalDivider() } - Text( + val scrollState = rememberScrollState() + val showBottomShadow by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } + Box( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - text = caption, - maxLines = 5, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodyLgRegular, - ) + .heightIn(max = if (hasCompactHeightWindowSize()) maxCaptionHeightLandscape else maxCaptionHeightPortrait), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(scrollState) + .navigationBarsPadding(), + text = caption, + style = ElementTheme.typography.fontBodyLgRegular, + ) + if (showBottomShadow) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .align(Alignment.BottomCenter) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + bgCanvasWithTransparency, + ), + ), + ), + ) + } + } } } } +private val maxCaptionHeightPortrait = 200.dp +private val maxCaptionHeightLandscape = 128.dp + @Composable private fun ThumbnailView( thumbnailSource: MediaSource?, @@ -604,3 +641,14 @@ internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider:: onBackClick = {}, ) } + +@Preview(device = "${Devices.PHONE}, orientation=landscape") +@Composable +internal fun MediaViewerViewLandscapePreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark { + MediaViewerView( + state = state, + audioFocus = null, + textFileViewer = { _, _ -> }, + onBackClick = {}, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..cb4da33df8 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,21 @@ + + + "このファイルはルームから削除され、他のユーザーは確認することができなくなります。" + "ファイルを削除しますか?" + "インターネット接続を確認した上、再度お試しください。" + "このルームに投稿された文書ファイルや音声ファイル・メッセージはここに表示されます。" + "アップロードされたファイルはありません" + "ファイルを読み込み中…" + "メディアを読み込み中…" + "ファイル" + "メディア" + "このルームに投稿された画像と動画はここに表示されます。" + "アップロードされたメディアはありません" + "ファイルとメディア" + "ファイル形式" + "ファイル名" + "これ以上ファイルはありません" + "これ以上メディアはありません" + "アップロード元" + "アップロード先" + diff --git a/libraries/permissions/api/src/main/res/values-ja/translations.xml b/libraries/permissions/api/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..1749b855a3 --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-ja/translations.xml @@ -0,0 +1,7 @@ + + + "カメラを使用するには、本体の設定から権限を付与する必要があります。" + "本体の設定から権限を付与してください。" + "マイクを使用するには、本体の設定から権限を付与してください。" + "通知を表示するには、本体の設定から権限を付与してください。" + diff --git a/libraries/permissions/api/src/main/res/values-vi/translations.xml b/libraries/permissions/api/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..c575d3b3bb --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-vi/translations.xml @@ -0,0 +1,7 @@ + + + "Để ứng dụng sử dụng camera, vui lòng cấp quyền trong cài đặt hệ thống." + "Vui lòng cấp quyền trong cài đặt hệ thống." + "Để ứng dụng có thể sử dụng micro, vui lòng cấp quyền trong cài đặt hệ thống." + "Để ứng dụng hiển thị thông báo, vui lòng cấp quyền trong cài đặt hệ thống." + diff --git a/libraries/permissions/impl/src/main/res/values-ja/translations.xml b/libraries/permissions/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..c29d26fd69 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,5 @@ + + + "アプリケーションが通知を表示できることを確認してください。" + "権限の確認" + diff --git a/libraries/permissions/impl/src/main/res/values-vi/translations.xml b/libraries/permissions/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..0f960c909e --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,5 @@ + + + "Kiểm tra xem ứng dụng có hiển thị thông báo hay không." + "Kiểm tra quyền truy cập" + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index cf76b26e64..64583bd1d4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -302,7 +302,6 @@ class DefaultNotifiableEventResolver( NotificationContent.StateEvent.PolicyRuleRoom, NotificationContent.StateEvent.PolicyRuleServer, NotificationContent.StateEvent.PolicyRuleUser, - NotificationContent.StateEvent.RoomAliases, NotificationContent.StateEvent.RoomAvatar, NotificationContent.StateEvent.RoomCanonicalAlias, NotificationContent.StateEvent.RoomCreate, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt index 9713110042..27a921c219 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlingWakeLock.kt @@ -14,7 +14,6 @@ import dev.zacsweers.metro.SingleIn import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.push.api.push.PushHandlingWakeLock import timber.log.Timber -import java.util.concurrent.atomic.AtomicInteger import kotlin.time.Duration @ContributesBinding(AppScope::class) @@ -22,24 +21,13 @@ import kotlin.time.Duration class DefaultPushHandlingWakeLock( @ApplicationContext private val context: Context, ) : PushHandlingWakeLock { - private val count = AtomicInteger(0) - override fun lock(time: Duration) { Timber.d("Acquiring wakelock for push handling, starting service.") FetchPushForegroundService.startIfNeeded(context) - - count.incrementAndGet() } override suspend fun unlock() { Timber.d("Releasing wakelock used for push handling.") FetchPushForegroundService.stop(context) - if (count.decrementAndGet() <= 0) { - Timber.d("No more wakelock needed for push handling, stopping service.") - count.set(0) - } else { - Timber.d("Wakelock still needed for push handling, restarting service | count: ${count.get()}.") - FetchPushForegroundService.startIfNeeded(context) - } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt index d7cc950b99..d54b7f5497 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/FetchPushForegroundService.kt @@ -17,6 +17,7 @@ import android.os.PowerManager import androidx.core.app.NotificationCompat import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.push.api.push.PushHandlingWakeLock @@ -57,6 +58,8 @@ class FetchPushForegroundService : Service() { } } + private var isOnForeground = false + override fun onCreate() { Timber.d("Creating FetchPushForegroundService") @@ -71,19 +74,39 @@ class FetchPushForegroundService : Service() { .setVibrate(longArrayOf(0)) .setSound(null) .build() - startForeground(NOTIFICATION_ID, notificationCompat) + + // Try to start the service in foreground. This can fail, even in cases where it's supposed to work according to the docs. + // In those cases we catch the exception and handle the failure so we don't try to start the wakelock or stop the service + // from running in foreground later. + runCatchingExceptions { + startForeground(NOTIFICATION_ID, notificationCompat) + } + .onSuccess { + isOnForeground = true + Timber.d("FetchPushForegroundService started in foreground successfully") + } + .onFailure { + isOnForeground = false + Timber.e(it, "Failed to start FetchPushForegroundService in foreground") + } super.onCreate() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (!isOnForeground) { + Timber.w("FetchPushForegroundService is not running in foreground, stopping it to avoid crash") + stopSelf() + return START_NOT_STICKY + } + wakelock.acquire(wakelockTimeout) // The timeout is not automatic before Android 15, so we need to schedule it ourselves if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { coroutineScope.launch { delay(wakelockTimeout) - onTimeout(startId) + onTimeoutAction(calledByTheSystem = false) } } @@ -91,18 +114,25 @@ class FetchPushForegroundService : Service() { } override fun stopService(intent: Intent?): Boolean { - wakelock.release() + if (isOnForeground) { + wakelock.release() + stopForeground(STOP_FOREGROUND_REMOVE) + } - stopForeground(STOP_FOREGROUND_REMOVE) return super.stopService(intent) } override fun onTimeout(startId: Int) { super.onTimeout(startId) + onTimeoutAction(calledByTheSystem = true) + } - Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService") - - coroutineScope.launch { pushHandlingWakeLock.unlock() } + private fun onTimeoutAction(calledByTheSystem: Boolean) { + Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") + if (isOnForeground) { + Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService") + coroutineScope.launch { pushHandlingWakeLock.unlock() } + } } companion object { @@ -119,7 +149,13 @@ class FetchPushForegroundService : Service() { fun start(context: Context) { val intent = Intent(context, FetchPushForegroundService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) + runCatchingExceptions { context.startForegroundService(intent) } + .onFailure { throwable -> + Timber.e( + throwable, + "Failed to start FetchPushForegroundService, notifications may take longer than usual to sync" + ) + } } else { context.startService(intent) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt index bdb8389feb..4c1da42660 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncPendingNotificationsRequestBuilder.kt @@ -76,7 +76,6 @@ class DefaultSyncPendingNotificationsRequestBuilder( .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) // If we're in an air-gapped environment, we shouldn't validate internet connectivity, as the checker will fail and the worker won't run at all. - // Note this will always be false for FOSS, since the feature is only enabled in Element Pro. if (networkMonitor.isInAirGappedEnvironment.first()) { Timber.d("In an air-gapped environment, not adding NET_CAPABILITY_VALIDATED to the network request") networkRequestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml index 9e44c2d7f9..6ed91c3b6b 100644 --- a/libraries/push/impl/src/main/res/values-it/translations.xml +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -15,6 +15,11 @@ "Non è stato possibile registrare il distributore di notifiche UnifiedPush, quindi non riceverai più notifiche. Controlla le impostazioni delle notifiche dell\'app e lo stato del distributore push." "Hai nuovi messaggi." + + "Hai %d nuovo messaggio." + "Hai %d nuovi messaggi." + + "📞 Chiamata in arrivo" "📹 Chiamata in arrivo" "** Invio fallito - si prega di aprire la stanza" "Entra" @@ -38,6 +43,8 @@ "%1$s ti ha invitato a unirti alla stanza" "Io" "%1$s ti ha menzionato o risposto" + "Ti abbiamo invitato a unirti allo spazio" + "%1$s ti ha invitato a unirti allo spazio" "Stai visualizzando la notifica! Cliccami!" "Discussione in %1$s" "%1$s: %2$s" diff --git a/libraries/push/impl/src/main/res/values-ja/translations.xml b/libraries/push/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f115a54042 --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,93 @@ + + + "通話" + "イベントを監視中" + "通常の通知" + "着信" + "サイレント通知" + + "%1$s: %2$d件のメッセージ" + + + "%d 件の通知" + + "Unified Push の通知配信サービス (notification distributor) を登録できないため、通知を受け取ることができません。通知の設定と通知ディストリビューター (push distributor) の状況を確認してください。" + "新着メッセージがあります。" + + "新着のメッセージが%d 件あります。" + + "📞 着信" + "📹 通話着信" + "** 送信失敗 - ルームを開いてください" + "参加" + "拒否" + + "%d 件の招待" + + "チャットにあなたを招待しました" + "%1$sがあなたをチャットに招待しました" + "%1$s があなたをメンションしました" + "新着メッセージ" + + "%d 件の新着メッセージ" + + "%1$sへのリアクション" + "既読にする" + "クイック返信" + "ルームに招待されました" + "%1$s があなたをルームに招待しました" + "自分" + "%1$s がメンションまたは返信しました" + "スペースに招待されました" + "%1$s があなたをスペースに招待しました" + "通知を表示しています。タップしてください。" + "%1$sのスレッド" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d件の未読メッセージ" + + "%1$sと%2$s" + "%2$sに%1$s" + "%2$sに%1$sと%3$s" + + "%d 個のルーム" + + "バックグラウンド同期" + "Google サービス" + "有効なGoogle Play 開発者サービスがありません。通知が正しく機能しない可能性があります。" + "ブロックしたユーザーを確認中" + "ブロックしたユーザーを表示" + "ブロックしたユーザーはいません。" + + "%1$d 人のユーザーをブロックしました。以降の通知を受信しません。" + + "ブロックしたユーザー" + "現在のプロバイダーの名前を取得してください。" + "プッシュ通知プロバイダーが選択されていません。" + "現在のプッシュ通知プロバイダーは %1$s で、現在のプッシュ通知ディストリビューター は %2$s です。しかし、ディストリビューター %3$s は見つかりませんでした。アンインストールされている可能性があります。" + "現在のプッシュ通知プロバイダーは %1$s ですが、ディストリビューターが設定されていません。" + "現在のプッシュ通知プロバイダー: %1$s" + "現在のプッシュ通知プロバイダー: %1$s (%2$s)" + "現在のプッシュ通知プロバイダー" + "少なくとも一つ以上のプッシュ通知プロバイダーに、アプリケーションが対応していることを確認してください。" + "対応しているプッシュ通知プロバイダーが見つかりませんでした。" + + "%1$d 個の通知プロバイダーを発見: %2$s" + + "このアプリケーションは %1$s に対応しています。" + "プッシュ通知プロバイダーへの対応状況" + "アプリケーションが通知を表示できることを確認してください。" + "通知がタップされていません。" + "通知を表示できません。" + "通知がタップされました。" + "通知の表示" + "テストを続行するには、通知にタップしてください。" + "プッシュ通知をアプリケーションが受信していることを確認してください。" + "エラー: プッシュ通知プロバイダーがリクエストを拒否しました。" + "エラー: %1$s" + "エラー: プッシュ通知をテストできません。" + "エラー: 通知の待機がタイムアウトしました。" + "プッシュ通知のループバックに %1$d ms 要しました。" + "プッシュ通知のループバックをテスト" + diff --git a/libraries/push/impl/src/main/res/values-ko/translations.xml b/libraries/push/impl/src/main/res/values-ko/translations.xml index 3aa015392f..f4399cda2b 100644 --- a/libraries/push/impl/src/main/res/values-ko/translations.xml +++ b/libraries/push/impl/src/main/res/values-ko/translations.xml @@ -16,6 +16,7 @@ "%d개의 새 메시지가 있습니다." + "📞 수신 전화" "📹 수신 전화" "** 전송 실패 - 방을 열여주세요" "참가하기" diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml index 24b1412500..c0b597588b 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -51,7 +51,7 @@ "Пригласил(а) вас в пространство" "%1$s пригласил(а) вас в пространство" "Вы просматриваете уведомление! Нажмите на меня!" - "Ветка в %1$s" + "Обсуждение в %1$s" "%1$s: %2$s" "%1$s: %2$s %3$s" diff --git a/libraries/push/impl/src/main/res/values-vi/translations.xml b/libraries/push/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..5592ff0e3a --- /dev/null +++ b/libraries/push/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,61 @@ + + + "Gọi" + "Đang lắng nghe sự kiện" + "Thông báo ồn ào" + "Cuộc gọi đang đổ chuông" + "Thông báo im lặng" + + "%1$s:%2$d tin nhắn" + + + "%d thông báo" + + "Bạn có tin nhắn mới." + "📹 Cuộc gọi đến" + "** Không gửi được - vui lòng mở phòng" + "Tham gia" + "Từ chối" + + "%d lời mời" + + "Đã mời bạn trò chuyện" + "Đã nhắc đến bạn: %1$s" + "Tin nhắn mới" + + "%dtin nhắn mới" + + "Đã thả %1$s vào tin nhắn" + "Đánh dấu đã đọc" + "Trả lời nhanh" + "Đã mời bạn tham gia phòng" + "Tôi" + "Bạn đang xem thông báo! Bấm vào đây!" + "%1$s:%2$s" + "%1$s: %2$s %3$s" + + "%dtin nhắn chưa đọc đã thông báo" + + "%1$s và %2$s" + "%1$s in %2$s" + "%1$s trong %2$s và %3$s" + + "%d phòng" + + "Đồng bộ hóa trong nền" + "Dịch vụ của Google" + "Không tìm thấy Dịch vụ Google Play hợp lệ. Thông báo có thể không hoạt động đúng cách." + "Người dùng bị chặn" + "Hãy đảm bảo rằng ứng dụng hỗ trợ ít nhất một nhà cung cấp thông báo đẩy." + "Không tìm thấy hỗ trợ từ nhà cung cấp thông báo đẩy." + + "Đã tìm thấy %1$d nhà cung cấp thông báo đẩy: %2$s" + + "Hỗ trợ nhà cung cấp thông báo đẩy" + "Kiểm tra xem ứng dụng có thể hiển thị thông báo hay không." + "Thông báo chưa được nhấp vào." + "Không thể hiển thị thông báo." + "Thông báo đã được nhấp!" + "Hiển thị thông báo" + "Hãy nhấp vào thông báo để tiếp tục thử nghiệm." + diff --git a/libraries/push/impl/src/main/res/values-zh/translations.xml b/libraries/push/impl/src/main/res/values-zh/translations.xml index 9a9057b07b..7e59c317c3 100644 --- a/libraries/push/impl/src/main/res/values-zh/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh/translations.xml @@ -16,6 +16,7 @@ "您有 %d 条新消息。" + "📞 来电" "📹 来电" "** 无法发送——请打开聊天室" "加入" @@ -57,7 +58,7 @@ "找不到有效的 Google Play 服务。通知可能无法正常工作。" "检查被阻止的用户" "查看被屏蔽的用户" - "没有用户被阻止。" + "未屏蔽任何用户。" "您已屏蔽 %1$d 位用户。您将不再收到这些用户的推送通知。" @@ -87,6 +88,6 @@ "错误:%1$s。" "错误,无法测试推送。" "错误,等待推送超时。" - "推送回路耗时%1$d 毫秒。" + "推送回路耗时 %1$d 毫秒。" "测试推送回路" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 63b903a3f7..33634e1767 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent -import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock @@ -835,7 +834,6 @@ class DefaultNotifiableEventResolverTest { testNoResults(NotificationContent.StateEvent.PolicyRuleRoom) testNoResults(NotificationContent.StateEvent.PolicyRuleServer) testNoResults(NotificationContent.StateEvent.PolicyRuleUser) - testNoResults(NotificationContent.StateEvent.RoomAliases) testNoResults(NotificationContent.StateEvent.RoomAvatar) testNoResults(NotificationContent.StateEvent.RoomCanonicalAlias) testNoResults(NotificationContent.StateEvent.RoomCreate) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index 9bba1c32d3..e9f6b76b7a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -18,7 +18,6 @@ import io.element.android.libraries.matrix.test.notification.FakeNotificationSer import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeCallNotificationEventResolver.kt similarity index 87% rename from libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeCallNotificationEventResolver.kt index f923d0c9fe..a1049683f5 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeCallNotificationEventResolver.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeCallNotificationEventResolver.kt @@ -6,11 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.push.test.notifications +package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.notification.NotificationData -import io.element.android.libraries.push.impl.notifications.CallNotificationEventResolver import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.tests.testutils.lambda.lambdaError diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt index a52eb16b07..2cf666d92a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.RoomInfo +import io.element.android.libraries.matrix.api.timeline.MsgType import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE @@ -341,9 +342,9 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply`() = runTest { - val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } + val sendMessage = lambdaRecorder, MsgType, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val replyMessage = - lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + lambdaRecorder, Boolean, MsgType, Result> { _, _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage replyMessageLambda = replyMessage @@ -375,7 +376,13 @@ class NotificationBroadcastReceiverHandlerTest { advanceUntilIdle() sendMessage.assertions() .isCalledOnce() - .with(value(A_MESSAGE), value(null), value(emptyList())) + .with( + value(A_MESSAGE), + value(null), + value(emptyList()), + value(MsgType.MSG_TYPE_TEXT), + value(false), + ) onNotifiableEventsReceivedResult.assertions() .isCalledOnce() replyMessage.assertions() @@ -384,7 +391,7 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply blank message`() = runTest { - val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } + val sendMessage = lambdaRecorder, MsgType, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage } @@ -408,9 +415,9 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test send reply to thread`() = runTest { - val sendMessage = lambdaRecorder, Result> { _, _, _ -> Result.success(Unit) } + val sendMessage = lambdaRecorder, MsgType, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } val replyMessage = - lambdaRecorder, Boolean, Result> { _, _, _, _, _ -> Result.success(Unit) } + lambdaRecorder, Boolean, MsgType, Result> { _, _, _, _, _, _ -> Result.success(Unit) } val liveTimeline = FakeTimeline().apply { sendMessageLambda = sendMessage replyMessageLambda = replyMessage @@ -453,7 +460,8 @@ class NotificationBroadcastReceiverHandlerTest { value(A_MESSAGE), value(null), value(emptyList()), - value(true) + value(true), + value(MsgType.MSG_TYPE_TEXT), ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index cc6e4674f9..a16568d400 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -26,8 +26,8 @@ import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.notifications.FakeNotificationResultProcessor import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler +import io.element.android.libraries.push.impl.workmanager.FakeSyncPendingNotificationsRequestBuilder import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder -import io.element.android.libraries.push.test.workmanager.FakeSyncPendingNotificationsRequestBuilder import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt index 796d5d192f..e65bea20a9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/DefaultSyncPendingNotificationsRequestBuilderTest.kt @@ -80,6 +80,9 @@ class DefaultSyncPendingNotificationsRequestBuilderTest { sessionId = A_SESSION_ID, sdkVersion = 33, isInAirGapEnvironment = false, + featureFlagService = FakeFeatureFlagService(initialState = mapOf( + FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching.key to true + )), ) val results = request.build() @@ -100,6 +103,9 @@ class DefaultSyncPendingNotificationsRequestBuilderTest { sessionId = A_SESSION_ID, sdkVersion = 33, isInAirGapEnvironment = true, + featureFlagService = FakeFeatureFlagService(initialState = mapOf( + FeatureFlags.ValidateNetworkWhenSchedulingNotificationFetching.key to true + )), ) val results = request.build() diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt similarity index 78% rename from libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt index ef0e38991e..f2da936b87 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FakeSyncPendingNotificationsRequestBuilder.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.push.test.workmanager +package io.element.android.libraries.push.impl.workmanager -import io.element.android.libraries.push.impl.workmanager.SyncPendingNotificationsRequestBuilder import io.element.android.libraries.workmanager.api.WorkManagerRequestWrapper class FakeSyncPendingNotificationsRequestBuilder( diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 475d4a4ae5..9dedeb3996 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { api(projects.libraries.push.api) api(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) - implementation(projects.libraries.push.impl) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.workmanager.api) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt index 67da09f1ab..3961f1f591 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.pushproviders.firebase import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.google.firebase.messaging.RemoteMessage.PRIORITY_HIGH import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag @@ -45,8 +46,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Timber.tag(loggerTag.value).w("New Firebase message. Priority: ${message.priority}/${message.originalPriority}") - // Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work - pushHandlingWakeLock.lock() + val isHighPriority = message.priority == PRIORITY_HIGH + if (isHighPriority) { + // Acquire wakelock to ensure the device stays awake while we handle the push and schedule and run the work + pushHandlingWakeLock.lock() + } coroutineScope.launch { val pushData = pushParser.parse(message.data) @@ -58,7 +62,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { "$it: ${message.data[it]}" }, ) - pushHandlingWakeLock.unlock() + if (isHighPriority) { + pushHandlingWakeLock.unlock() + } } else { val handled = pushHandler.handle( pushData = pushData, @@ -66,7 +72,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { ) // If we failed to handle the push, we should release the wakelock early to avoid keeping the device awake for too long. - if (!handled) { + if (!handled && isHighPriority) { pushHandlingWakeLock.unlock() } } diff --git a/libraries/pushproviders/firebase/src/main/res/values-ja/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..5c07e2ad79 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ja/translations.xml @@ -0,0 +1,11 @@ + + + "Firebase が利用可能であることを確認してください。" + "Firebase を利用できません。" + "Firebase は利用可能です。" + "Firebase の確認" + "Firebase トークンが利用可能であることを確認してください。" + "Firebase トークンが不明です。" + "Firebase トークン: %1$s" + "Firebase トークンの確認" + diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt index 87ca74730c..798328e626 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt @@ -96,6 +96,7 @@ class VectorFirebaseMessagingServiceTest { putString("event_id", AN_EVENT_ID.value) putString("room_id", A_ROOM_ID.value) putString("cs", A_SECRET) + putString("google.priority", "high") }, ) ) @@ -127,6 +128,7 @@ class VectorFirebaseMessagingServiceTest { putString("event_id", AN_EVENT_ID.value) putString("room_id", A_ROOM_ID.value) putString("cs", A_SECRET) + putString("google.priority", "high") }, ) ) @@ -141,6 +143,33 @@ class VectorFirebaseMessagingServiceTest { unlockLambda.assertions().isCalledOnce() } + @Test + fun `test pushHandler with a remote message with normal priority won't lock the wakelock`() = runTest { + val lockLambda = lambdaRecorder { _ -> } + val unlockLambda = lambdaRecorder { } + val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( + pushHandler = FakePushHandler(handleResult = { _, _ -> false }), + pushHandlingWakeLock = FakePushHandlingWakeLock( + lock = lockLambda, + unlock = unlockLambda + ) + ) + vectorFirebaseMessagingService.onMessageReceived( + message = RemoteMessage( + Bundle().apply { + putString("event_id", AN_EVENT_ID.value) + putString("room_id", A_ROOM_ID.value) + putString("cs", A_SECRET) + putString("google.priority", "normal") + }, + ) + ) + + // The wakelock should not be locked + lockLambda.assertions().isNeverCalled() + unlockLambda.assertions().isNeverCalled() + } + @Test fun `test new token is forwarded to the handler`() = runTest { val lambda = lambdaRecorder { } diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ja/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..a081ba04ff --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ja/translations.xml @@ -0,0 +1,9 @@ + + + "UnifiedPush のディストリビューターが利用可能であることを確認してください。" + "プッシュ通知ディストリビューターが見つかりませんでした。" + + "%1$d 個のディストリビューターを発見: %2$s" + + "UnifiedPush を確認" + diff --git a/libraries/slashcommands/api/build.gradle.kts b/libraries/slashcommands/api/build.gradle.kts new file mode 100644 index 0000000000..8cec0e65af --- /dev/null +++ b/libraries/slashcommands/api/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.slashcommands.api" +} + +dependencies { + implementation(projects.libraries.matrix.api) +} diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/ChatEffect.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/ChatEffect.kt new file mode 100644 index 0000000000..7b31ffb3b7 --- /dev/null +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/ChatEffect.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.api + +enum class ChatEffect { + CONFETTI, + SNOWFALL +} diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/MessagePrefix.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/MessagePrefix.kt new file mode 100644 index 0000000000..713458c720 --- /dev/null +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/MessagePrefix.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.api + +enum class MessagePrefix { + Shrug, + TableFlip, + Unflip, + Lenny, +} diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt new file mode 100644 index 0000000000..50d5a5ce32 --- /dev/null +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommand.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.api + +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Represent a slash command. + */ +sealed interface SlashCommand { + // This is not a Slash command + data object NotACommand : SlashCommand + + // Slash command types: + sealed interface Error : SlashCommand + sealed interface SlashCommandSendMessage : SlashCommand + sealed interface SlashCommandAdmin : SlashCommand + sealed interface SlashCommandNavigation : SlashCommand + + // Errors + data class ErrorEmptySlashCommand(val message: String) : Error + data class ErrorCommandNotSupportedInThreads(val message: String) : Error + + // Unknown/Unsupported slash command + data class ErrorUnknownSlashCommand(val message: String) : Error + + // A slash command is detected, but there is an error + data class ErrorSyntax(val message: String) : Error + + // Valid commands: + data class SendPlainText(val message: CharSequence) : SlashCommandSendMessage + data class SendEmote(val message: CharSequence) : SlashCommandSendMessage + data class SendRainbow(val message: CharSequence) : SlashCommandSendMessage + data class SendRainbowEmote(val message: CharSequence) : SlashCommandSendMessage + data class BanUser(val userId: UserId, val reason: String?) : SlashCommandAdmin + data class UnbanUser(val userId: UserId, val reason: String?) : SlashCommandAdmin + data class IgnoreUser(val userId: UserId) : SlashCommandAdmin + data class UnignoreUser(val userId: UserId) : SlashCommandAdmin + data class SetUserPowerLevel(val userId: UserId, val powerLevel: Int?) : SlashCommandAdmin + data class ChangeRoomName(val name: String) : SlashCommandAdmin + data class Invite(val userId: UserId, val reason: String?) : SlashCommandAdmin + data class JoinRoom(val roomIdOrAlias: RoomIdOrAlias, val reason: String?) : SlashCommandAdmin + data class ChangeTopic(val topic: String) : SlashCommandAdmin + data class RemoveUser(val userId: UserId, val reason: String?) : SlashCommandAdmin + data class ChangeDisplayName(val displayName: String) : SlashCommandAdmin + data class ChangeDisplayNameForRoom(val displayName: String) : SlashCommandAdmin + data class ChangeRoomAvatar(val url: String) : SlashCommandAdmin + data class ChangeAvatar(val url: String) : SlashCommandAdmin + data class ChangeAvatarForRoom(val url: String) : SlashCommandAdmin + data class SendSpoiler(val message: String) : SlashCommandSendMessage + data class SendWithPrefix(val prefix: MessagePrefix, val message: CharSequence) : SlashCommandSendMessage + data object DiscardSession : SlashCommandAdmin + data class SendChatEffect(val chatEffect: ChatEffect, val message: String) : SlashCommandSendMessage + data object LeaveRoom : SlashCommandAdmin + data class UpgradeRoom(val newVersion: String) : SlashCommandAdmin + + data object DevTools : SlashCommandNavigation + data class ShowUser(val userId: UserId) : SlashCommandNavigation +} + +fun SlashCommand.Error.message() = when (this) { + is SlashCommand.ErrorEmptySlashCommand -> message + is SlashCommand.ErrorCommandNotSupportedInThreads -> message + is SlashCommand.ErrorUnknownSlashCommand -> message + is SlashCommand.ErrorSyntax -> message +} diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt new file mode 100644 index 0000000000..9dfca26078 --- /dev/null +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.api + +import io.element.android.libraries.matrix.api.timeline.Timeline + +interface SlashCommandService { + suspend fun getSuggestions( + text: String, + isInThread: Boolean, + ): List + + /** + * Parse the message and return a SlashCommand. + */ + suspend fun parse( + textMessage: CharSequence, + formattedMessage: String?, + isInThreadTimeline: Boolean, + ): SlashCommand + + /** + * Proceed a SlashCommandSendMessage. + */ + suspend fun proceedSendMessage( + slashCommand: SlashCommand.SlashCommandSendMessage, + timeline: Timeline, + ): Result + + /** + * Proceed a SlashCommandAdmin. + */ + suspend fun proceedAdmin( + slashCommand: SlashCommand.SlashCommandAdmin, + ): Result +} diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandSuggestion.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandSuggestion.kt new file mode 100644 index 0000000000..5a826d5fbd --- /dev/null +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandSuggestion.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.api + +data class SlashCommandSuggestion( + val command: String, + val parameters: String?, + val description: String, +) diff --git a/libraries/slashcommands/impl/build.gradle.kts b/libraries/slashcommands/impl/build.gradle.kts new file mode 100644 index 0000000000..34dc2e42b2 --- /dev/null +++ b/libraries/slashcommands/impl/build.gradle.kts @@ -0,0 +1,35 @@ +import extension.setupDependencyInjection +import extension.testCommonDependencies + +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.slashcommands.impl" +} + +setupDependencyInjection() + +dependencies { + implementation(projects.libraries.androidutils) + implementation(projects.libraries.preferences.api) + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) + api(projects.libraries.slashcommands.api) + implementation(projects.libraries.di) + implementation(projects.libraries.featureflag.api) + implementation(projects.services.toolbox.api) + + testCommonDependencies(libs) + testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.toolbox.test) +} diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt new file mode 100644 index 0000000000..0d9e1e8c72 --- /dev/null +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/Command.kt @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import androidx.annotation.StringRes + +/** + * Defines the command line operations. + * The user can write these messages to perform some actions. + * The list will be displayed in this order. + */ +enum class Command( + val command: String, + val aliases: List? = null, + val parameters: String? = null, + @StringRes val description: Int, + val isAllowedInThread: Boolean = true, + val isSupported: Boolean = true, + val isDevCommand: Boolean = false, +) { + CRASH_APP( + command = "/crash", + description = R.string.slash_command_description_crash_application, + isDevCommand = true, + ), + EMOTE( + command = "/me", + parameters = "", + description = R.string.slash_command_description_emote, + ), + BAN_USER( + command = "/ban", + parameters = " [reason]", + description = R.string.slash_command_description_ban_user, + ), + UNBAN_USER( + command = "/unban", + parameters = " [reason]", + description = R.string.slash_command_description_unban_user, + ), + IGNORE_USER( + command = "/ignore", + parameters = " [reason]", + description = R.string.slash_command_description_ignore_user, + ), + UNIGNORE_USER( + command = "/unignore", + parameters = "", + description = R.string.slash_command_description_unignore_user, + ), + SET_USER_POWER_LEVEL( + command = "/op", + parameters = " []", + description = R.string.slash_command_description_op_user, + isAllowedInThread = false, + isSupported = false, + ), + RESET_USER_POWER_LEVEL( + command = "/deop", + parameters = "", + description = R.string.slash_command_description_deop_user, + isAllowedInThread = false, + isSupported = false, + ), + ROOM_NAME( + command = "/roomname", + parameters = "", + description = R.string.slash_command_description_room_name, + isAllowedInThread = false, + ), + INVITE( + command = "/invite", + parameters = " [reason]", + description = R.string.slash_command_description_invite_user, + ), + JOIN_ROOM( + command = "/join", + aliases = listOf("/j", "/goto"), + parameters = " [reason]", + description = R.string.slash_command_description_join_room, + isAllowedInThread = false, + isSupported = false, + ), + TOPIC( + command = "/topic", + parameters = "", + description = R.string.slash_command_description_topic, + isAllowedInThread = false, + ), + REMOVE_USER( + command = "/remove", + aliases = listOf("/kick"), + parameters = " [reason]", + description = R.string.slash_command_description_remove_user, + ), + CHANGE_DISPLAY_NAME( + command = "/nick", + parameters = "", + description = R.string.slash_command_description_nick, + ), + CHANGE_DISPLAY_NAME_FOR_ROOM( + command = "/myroomnick", + aliases = listOf("/roomnick"), + parameters = "", + description = R.string.slash_command_description_nick_for_room, + isAllowedInThread = false, + isSupported = false, + ), + ROOM_AVATAR( + command = "/roomavatar", + parameters = "", + description = R.string.slash_command_description_room_avatar, + isAllowedInThread = false, + // Dev command since user has to know the mxc url + isDevCommand = true, + isSupported = false, + ), + CHANGE_AVATAR( + command = "/myavatar", + parameters = "", + description = R.string.slash_command_description_avatar, + isAllowedInThread = false, + // Dev command since user has to know the mxc url + isDevCommand = true, + isSupported = false, + ), + CHANGE_AVATAR_FOR_ROOM( + command = "/myroomavatar", + parameters = "", + description = R.string.slash_command_description_avatar_for_room, + isAllowedInThread = false, + // Dev command since user has to know the mxc url + isDevCommand = true, + isSupported = false, + ), + RAINBOW( + command = "/rainbow", + parameters = "", + description = R.string.slash_command_description_rainbow, + ), + RAINBOW_EMOTE( + command = "/rainbowme", + parameters = "", + description = R.string.slash_command_description_rainbow_emote, + ), + DEVTOOLS( + command = "/devtools", + description = R.string.slash_command_description_devtools, + isDevCommand = true, + ), + SPOILER( + command = "/spoiler", + parameters = "", + description = R.string.slash_command_description_spoiler, + ), + SHRUG( + command = "/shrug", + parameters = "", + description = R.string.slash_command_description_shrug, + ), + LENNY( + command = "/lenny", + parameters = "", + description = R.string.slash_command_description_lenny, + ), + PLAIN( + command = "/plain", + parameters = "", + description = R.string.slash_command_description_plain, + ), + WHOIS( + command = "/whois", + parameters = "", + description = R.string.slash_command_description_whois, + ), + DISCARD_SESSION( + command = "/discardsession", + description = R.string.slash_command_description_discard_session, + isAllowedInThread = false, + isSupported = false, + ), + CONFETTI( + command = "/confetti", + parameters = "", + description = R.string.slash_command_confetti, + isAllowedInThread = false, + isSupported = false, + ), + SNOWFALL( + command = "/snowfall", + parameters = "", + description = R.string.slash_command_snow, + isAllowedInThread = false, + isSupported = false, + ), + LEAVE_ROOM( + command = "/leave", + aliases = listOf("/part"), + description = R.string.slash_command_description_leave_room, + isAllowedInThread = false, + isDevCommand = true, + ), + UPGRADE_ROOM( + command = "/upgraderoom", + parameters = "newVersion", + description = R.string.slash_command_description_upgrade_room, + isAllowedInThread = false, + isDevCommand = true, + isSupported = false, + ), + TABLE_FLIP( + command = "/tableflip", + parameters = "", + description = R.string.slash_command_description_table_flip, + ), + UNFLIP( + command = "/unflip", + parameters = "", + description = R.string.slash_command_description_unflip, + ); + + val allAliases = listOf(command) + aliases.orEmpty() + + /** + * Checks if the input command matches any of the command aliases, ignoring case. + * Do not exclude not supported commands so that user can discover that the command is not supported. + * Used for whole command parsing. + */ + fun matches(inputCommand: CharSequence) = allAliases.any { it.contentEquals(inputCommand, true) } + + /** + * Checks if the input is a prefix of any of the command aliases, ignoring the first character (the slash), and excluding not supported command. + * Used for suggestions. + */ + fun startsWith(input: CharSequence) = isSupported && + allAliases.any { it.startsWith(input, 1, true) } +} diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt new file mode 100644 index 0000000000..ad252cb224 --- /dev/null +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutor.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.timeline.MsgType +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.slashcommands.api.MessagePrefix +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.impl.rainbow.RainbowGenerator +import io.element.android.services.toolbox.api.strings.StringProvider + +@Inject +class CommandExecutor( + private val matrixClient: MatrixClient, + private val joinedRoom: JoinedRoom, + private val rainbowGenerator: RainbowGenerator, + private val stringProvider: StringProvider, +) { + suspend fun proceedSendMessage( + slashCommand: SlashCommand.SlashCommandSendMessage, + timeline: Timeline, + ): Result { + return when (slashCommand) { + is SlashCommand.SendChatEffect -> sendChatEffect() + is SlashCommand.SendEmote -> sendEmote(slashCommand, timeline) + is SlashCommand.SendWithPrefix -> sendPrefixedMessage(slashCommand.prefix, slashCommand.message, timeline) + is SlashCommand.SendPlainText -> sendPlainText(slashCommand, timeline) + is SlashCommand.SendRainbow -> sendRainbow(slashCommand, timeline) + is SlashCommand.SendRainbowEmote -> sendRainbowEmote(slashCommand, timeline) + is SlashCommand.SendSpoiler -> sendSpoiler(slashCommand, timeline) + } + } + + suspend fun proceedAdmin( + slashCommand: SlashCommand.SlashCommandAdmin, + ): Result { + return when (slashCommand) { + is SlashCommand.BanUser -> banUser(slashCommand) + is SlashCommand.ChangeAvatar -> changeAvatar() + is SlashCommand.ChangeAvatarForRoom -> changeAvatarForRoom() + is SlashCommand.ChangeDisplayName -> changeDisplayName(slashCommand) + is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom() + is SlashCommand.ChangeRoomAvatar -> changeRoomAvatar() + is SlashCommand.ChangeRoomName -> changeRoomName(slashCommand) + is SlashCommand.ChangeTopic -> changeTopic(slashCommand) + is SlashCommand.DiscardSession -> discardSession() + is SlashCommand.IgnoreUser -> ignoreUser(slashCommand) + is SlashCommand.Invite -> invite(slashCommand) + is SlashCommand.JoinRoom -> joinRoom(slashCommand) + is SlashCommand.LeaveRoom -> leaveRoom(joinedRoom) + is SlashCommand.RemoveUser -> removeUser(slashCommand) + is SlashCommand.SetUserPowerLevel -> setUserPowerLevel() + is SlashCommand.UnbanUser -> unbanUser(slashCommand) + is SlashCommand.UnignoreUser -> unignoreUser(slashCommand) + is SlashCommand.UpgradeRoom -> upgradeRoom() + } + } + + private fun upgradeRoom(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private suspend fun unignoreUser(slashCommand: SlashCommand.UnignoreUser): Result { + return matrixClient.unignoreUser(slashCommand.userId) + } + + private suspend fun unbanUser(slashCommand: SlashCommand.UnbanUser): Result { + return joinedRoom.unbanUser(slashCommand.userId, slashCommand.reason) + } + + private fun setUserPowerLevel(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private suspend fun sendSpoiler(slashCommand: SlashCommand.SendSpoiler, timeline: Timeline): Result { + val text = "[${stringProvider.getString(R.string.common_spoiler)}](${slashCommand.message})" + val formattedText = "${slashCommand.message}" + return timeline.sendMessage( + body = text, + htmlBody = formattedText, + intentionalMentions = emptyList(), + ) + } + + private suspend fun sendRainbowEmote(slashCommand: SlashCommand.SendRainbowEmote, timeline: Timeline): Result { + val message = slashCommand.message.toString() + return timeline.sendMessage( + body = message, + htmlBody = rainbowGenerator.generate(message), + msgType = MsgType.MSG_TYPE_EMOTE, + intentionalMentions = emptyList(), + ) + } + + private suspend fun sendRainbow(slashCommand: SlashCommand.SendRainbow, timeline: Timeline): Result { + val message = slashCommand.message.toString() + return timeline.sendMessage( + body = message, + htmlBody = rainbowGenerator.generate(message), + intentionalMentions = emptyList(), + ) + } + + private suspend fun sendPlainText(slashCommand: SlashCommand.SendPlainText, timeline: Timeline): Result { + return timeline.sendMessage( + body = slashCommand.message.toString(), + htmlBody = null, + intentionalMentions = emptyList(), + asPlainText = true, + ) + } + + private suspend fun sendEmote(slashCommand: SlashCommand.SendEmote, timeline: Timeline): Result { + val message = slashCommand.message.toString() + return timeline.sendMessage( + body = message, + htmlBody = null, + msgType = MsgType.MSG_TYPE_EMOTE, + intentionalMentions = emptyList(), + ) + } + + private fun sendChatEffect(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private suspend fun removeUser(slashCommand: SlashCommand.RemoveUser): Result { + return joinedRoom.kickUser(slashCommand.userId, slashCommand.reason) + } + + private suspend fun leaveRoom( + room: JoinedRoom, + ): Result { + return room.leave() + } + + private suspend fun joinRoom(slashCommand: SlashCommand.JoinRoom): Result { + return matrixClient.joinRoomByIdOrAlias(slashCommand.roomIdOrAlias, emptyList()) + .map {} + } + + private suspend fun invite(slashCommand: SlashCommand.Invite): Result { + return joinedRoom.inviteUserById(slashCommand.userId) + } + + private suspend fun ignoreUser(slashCommand: SlashCommand.IgnoreUser): Result { + return matrixClient.ignoreUser(slashCommand.userId) + } + + private fun discardSession(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private suspend fun changeTopic(slashCommand: SlashCommand.ChangeTopic): Result { + return joinedRoom.setTopic(slashCommand.topic) + } + + private suspend fun changeRoomName(slashCommand: SlashCommand.ChangeRoomName): Result { + return joinedRoom.setName(slashCommand.name) + } + + private fun changeRoomAvatar(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private fun changeDisplayNameForRoom(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private suspend fun changeDisplayName(slashCommand: SlashCommand.ChangeDisplayName): Result { + return matrixClient.setDisplayName(slashCommand.displayName) + } + + private fun changeAvatar(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private fun changeAvatarForRoom(): Result { + return Result.failure(Exception("Not yet implemented")) + } + + private suspend fun banUser(slashCommand: SlashCommand.BanUser): Result { + return joinedRoom.banUser(slashCommand.userId, slashCommand.reason) + } + + private suspend fun sendPrefixedMessage( + prefix: MessagePrefix, + message: CharSequence, + timeline: Timeline, + ): Result { + val sequence = buildString { + append(prefix.toMarkdown()) + if (message.isNotEmpty()) { + append(" ") + append(message) + } + } + return timeline.sendMessage( + body = sequence, + htmlBody = null, + intentionalMentions = emptyList(), + ) + } +} + +private fun MessagePrefix.toMarkdown() = when (this) { + MessagePrefix.Shrug -> "¯\\\\_(ツ)\\_/¯" + MessagePrefix.TableFlip -> "(╯°□°)╯︵ ┻━┻" + MessagePrefix.Unflip -> "┬──┬ ノ( ゜-゜ノ)" + MessagePrefix.Lenny -> "( ͡° ͜ʖ ͡°)" +} diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt new file mode 100644 index 0000000000..55125af20b --- /dev/null +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/CommandParser.kt @@ -0,0 +1,442 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.core.MatrixPatterns +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.mxc.isMxcUrl +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.slashcommands.api.ChatEffect +import io.element.android.libraries.slashcommands.api.MessagePrefix +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.flow.first +import timber.log.Timber + +@Inject +class CommandParser( + private val appPreferencesStore: AppPreferencesStore, + private val featureFlagService: FeatureFlagService, + private val stringProvider: StringProvider, +) { + /** + * Convert the text message into a Slash command. + * + * @param textMessage the text message in plain text + * @param formattedMessage the text messaged in HTML format + * @param isInThreadTimeline true if the user is currently typing in a thread + * @return a parsed slash command (ok or error) + */ + suspend fun parseSlashCommand( + textMessage: CharSequence, + formattedMessage: String?, + isInThreadTimeline: Boolean, + ): SlashCommand { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SlashCommand)) { + return SlashCommand.NotACommand + } + // check if it has the Slash marker + val message = formattedMessage ?: textMessage + return if (!message.startsWith("/")) { + SlashCommand.NotACommand + } else { + // "/" only + if (message.length == 1) { + return SlashCommand.ErrorEmptySlashCommand( + stringProvider.getString(R.string.slash_command_unrecognized, "/") + ) + } + // Exclude "//" + if ("/" == message.substring(1, 2)) { + return SlashCommand.NotACommand + } + val (messageParts, message) = extractMessage(message.toString()) + ?: return SlashCommand.ErrorEmptySlashCommand( + stringProvider.getString(R.string.slash_command_unrecognized, "/") + ) + val slashCommand = messageParts.first() + getNotSupportedByThreads(isInThreadTimeline, slashCommand)?.let { + return SlashCommand.ErrorCommandNotSupportedInThreads( + stringProvider.getString( + R.string.slash_command_not_supported_in_threads, + it.command, + ) + ) + } + when { + Command.PLAIN.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.SendPlainText(message = message) + } else { + syntaxError(Command.PLAIN) + } + } + Command.CHANGE_DISPLAY_NAME.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.ChangeDisplayName(displayName = message) + } else { + syntaxError(Command.CHANGE_DISPLAY_NAME) + } + } + Command.CHANGE_DISPLAY_NAME_FOR_ROOM.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.ChangeDisplayNameForRoom(displayName = message) + } else { + syntaxError(Command.CHANGE_DISPLAY_NAME_FOR_ROOM) + } + } + Command.ROOM_AVATAR.matches(slashCommand) -> { + if (messageParts.size == 2) { + val url = messageParts[1] + if (url.isMxcUrl()) { + SlashCommand.ChangeRoomAvatar(url) + } else { + syntaxError(Command.ROOM_AVATAR) + } + } else { + syntaxError(Command.ROOM_AVATAR) + } + } + Command.CHANGE_AVATAR.matches(slashCommand) -> { + if (messageParts.size == 2) { + val url = messageParts[1] + if (url.isMxcUrl()) { + SlashCommand.ChangeAvatar(url) + } else { + syntaxError(Command.CHANGE_AVATAR) + } + } else { + syntaxError(Command.CHANGE_AVATAR) + } + } + Command.CHANGE_AVATAR_FOR_ROOM.matches(slashCommand) -> { + if (messageParts.size == 2) { + val url = messageParts[1] + + if (url.isMxcUrl()) { + SlashCommand.ChangeAvatarForRoom(url) + } else { + syntaxError(Command.CHANGE_AVATAR_FOR_ROOM) + } + } else { + syntaxError(Command.CHANGE_AVATAR_FOR_ROOM) + } + } + Command.TOPIC.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.ChangeTopic(topic = message) + } else { + syntaxError(Command.TOPIC) + } + } + Command.EMOTE.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.SendEmote(message) + } else { + syntaxError(Command.EMOTE) + } + } + Command.RAINBOW.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.SendRainbow(message) + } else { + syntaxError(Command.RAINBOW) + } + } + Command.RAINBOW_EMOTE.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.SendRainbowEmote(message) + } else { + syntaxError(Command.RAINBOW_EMOTE) + } + } + Command.JOIN_ROOM.matches(slashCommand) -> { + if (messageParts.size >= 2) { + val id = messageParts[1] + val roomIdOrAlias = RoomIdOrAlias.from(id) + if (roomIdOrAlias != null) { + SlashCommand.JoinRoom( + RoomIdOrAlias.Id(RoomId(id)), + trimParts(textMessage, messageParts.take(2)) + ) + } else { + syntaxError(Command.JOIN_ROOM) + } + } else { + syntaxError(Command.JOIN_ROOM) + } + } + Command.ROOM_NAME.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.ChangeRoomName(name = message) + } else { + syntaxError(Command.ROOM_NAME) + } + } + Command.INVITE.matches(slashCommand) -> { + if (messageParts.size >= 2) { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.Invite( + userId = userId, + reason = trimParts(textMessage, messageParts.take(2)) + ) + } + ?: syntaxError(Command.INVITE) + } else { + syntaxError(Command.INVITE) + } + } + Command.REMOVE_USER.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.RemoveUser( + userId = userId, + reason = trimParts(textMessage, messageParts.take(2)) + ) + } + ?: syntaxError(Command.REMOVE_USER) + } + Command.BAN_USER.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.BanUser( + userId = userId, + reason = trimParts(textMessage, messageParts.take(2)) + ) + } + ?: syntaxError(Command.BAN_USER) + } + Command.UNBAN_USER.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.UnbanUser( + userId = userId, + reason = trimParts(textMessage, messageParts.take(2)) + ) + } + ?: syntaxError(Command.UNBAN_USER) + } + Command.IGNORE_USER.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.IgnoreUser( + userId = userId, + ) + } + ?: syntaxError(Command.IGNORE_USER) + } + Command.UNIGNORE_USER.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.UnignoreUser( + userId = userId, + ) + } + ?: syntaxError(Command.UNIGNORE_USER) + } + Command.SET_USER_POWER_LEVEL.matches(slashCommand) -> { + if (messageParts.size == 3) { + val userId = parseUserId(messageParts) + if (userId != null) { + val powerLevelsAsString = messageParts[2] + try { + val powerLevelsAsInt = Integer.parseInt(powerLevelsAsString) + SlashCommand.SetUserPowerLevel( + userId = userId, + powerLevel = powerLevelsAsInt + ) + } catch (_: Exception) { + syntaxError(Command.SET_USER_POWER_LEVEL) + } + } else { + syntaxError(Command.SET_USER_POWER_LEVEL) + } + } else { + syntaxError(Command.SET_USER_POWER_LEVEL) + } + } + Command.RESET_USER_POWER_LEVEL.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.SetUserPowerLevel( + userId = userId, + powerLevel = null + ) + } + ?: syntaxError(Command.SET_USER_POWER_LEVEL) + } + Command.DEVTOOLS.matches(slashCommand) -> { + if (messageParts.size == 1) { + SlashCommand.DevTools + } else { + syntaxError(Command.DEVTOOLS) + } + } + Command.SPOILER.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.SendSpoiler(message) + } else { + syntaxError(Command.SPOILER) + } + } + Command.SHRUG.matches(slashCommand) -> { + SlashCommand.SendWithPrefix(MessagePrefix.Shrug, message) + } + Command.LENNY.matches(slashCommand) -> { + SlashCommand.SendWithPrefix(MessagePrefix.Lenny, message) + } + Command.TABLE_FLIP.matches(slashCommand) -> { + SlashCommand.SendWithPrefix(MessagePrefix.TableFlip, message) + } + Command.UNFLIP.matches(slashCommand) -> { + SlashCommand.SendWithPrefix(MessagePrefix.Unflip, message) + } + Command.DISCARD_SESSION.matches(slashCommand) -> { + if (messageParts.size == 1) { + SlashCommand.DiscardSession + } else { + syntaxError(Command.DISCARD_SESSION) + } + } + Command.WHOIS.matches(slashCommand) -> { + parseUserId(messageParts) + ?.let { userId -> + SlashCommand.ShowUser( + userId = userId, + ) + } + ?: syntaxError(Command.WHOIS) + } + Command.CONFETTI.matches(slashCommand) -> { + SlashCommand.SendChatEffect(ChatEffect.CONFETTI, message) + } + Command.SNOWFALL.matches(slashCommand) -> { + SlashCommand.SendChatEffect(ChatEffect.SNOWFALL, message) + } + Command.LEAVE_ROOM.matches(slashCommand) -> { + if (messageParts.size == 1) { + SlashCommand.LeaveRoom + } else { + syntaxError(Command.LEAVE_ROOM) + } + } + Command.UPGRADE_ROOM.matches(slashCommand) -> { + if (message.isNotEmpty()) { + SlashCommand.UpgradeRoom(newVersion = message) + } else { + syntaxError(Command.UPGRADE_ROOM) + } + } + Command.CRASH_APP.matches(slashCommand) && appPreferencesStore.isDeveloperModeEnabledFlow().first() -> { + error("Application crashed from user demand") + } + else -> { + // Unknown command + SlashCommand.ErrorUnknownSlashCommand( + stringProvider.getString(R.string.slash_command_unrecognized, slashCommand) + ) + } + } + } + } + + private fun parseUserId(messageParts: List): UserId? { + val str = messageParts.getOrNull(1) ?: return null + return when { + MatrixPatterns.isUserId(str) -> str + str == " { + // Rich text editor mode + messageParts.getOrNull(2)?.let { html -> + // html must match "href="https://matrix.to/#/@user:domain.org">@user:domain.org" + val regex = "href=\"https://matrix.to/#/([^\"]+)\">([^<]+)".toRegex() + val matchResult = regex.find(html) + val userId = matchResult?.groupValues?.getOrNull(1) + userId?.takeIf { + userId == matchResult.groupValues.getOrNull(2) && MatrixPatterns.isUserId(it) + } + } + } + else -> { + // Can be markdown format like "[@user:domain.org](https://matrix.to/#/@user:domain.org)" + val regex = "\\[([^\\]]+)]\\(https://matrix.to/#/([^\\]]+)\\)".toRegex() + val matchResult = regex.find(str) + val userId = matchResult?.groupValues?.getOrNull(1) + userId?.takeIf { + userId == matchResult.groupValues.getOrNull(2) && MatrixPatterns.isUserId(it) + } + } + } + ?.let(::UserId) + } + + private fun syntaxError(command: Command) = SlashCommand.ErrorSyntax( + stringProvider.getString( + R.string.slash_command_parameters_error, + command.command, + buildString { + append(command.command) + if (command.parameters != null) { + append(" ${command.parameters}") + } + }, + ) + ) + + private fun extractMessage(message: String): Pair, String>? { + val messageParts = try { + message.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() } + } catch (e: Exception) { + Timber.e(e, "## parseSlashCommand() : split failed") + null + } + + // test if the string cut fails + if (messageParts.isNullOrEmpty()) { + return null + } + + val slashCommand = messageParts.first() + val trimmedMessage = message.substring(slashCommand.length).trim() + + return messageParts to trimmedMessage + } + + private val notSupportedThreadsCommands: List by lazy { + Command.entries.filter { + !it.isAllowedInThread + } + } + + /** + * Checks whether the current command is not supported by threads. + * @param isInThreadTimeline if its true we are in a thread timeline + * @param slashCommand the slash command that will be checked + * @return The command that is not supported + */ + private fun getNotSupportedByThreads(isInThreadTimeline: Boolean, slashCommand: String): Command? { + return if (isInThreadTimeline) { + notSupportedThreadsCommands.firstOrNull { + it.command == slashCommand + } + } else { + null + } + } + + private fun trimParts(message: CharSequence, messageParts: List): String? { + val partsSize = messageParts.sumOf { it.length } + val gapsNumber = messageParts.size - 1 + return message.substring(partsSize + gapsNumber).trim().takeIf { it.isNotEmpty() } + } +} diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt new file mode 100644 index 0000000000..6cd8688cad --- /dev/null +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandService.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds + +@ContributesBinding(RoomScope::class) +class DefaultSlashCommandService( + private val commandParser: CommandParser, + private val commandExecutor: CommandExecutor, + private val stringProvider: StringProvider, + private val appPreferencesStore: AppPreferencesStore, + private val featureFlagService: FeatureFlagService, + private val capabilitiesProvider: HomeserverCapabilitiesProvider, +) : SlashCommandService { + override suspend fun getSuggestions( + text: String, + isInThread: Boolean, + ): List { + if (!featureFlagService.isFeatureEnabled(FeatureFlags.SlashCommand)) return emptyList() + val isDeveloperModeEnabled = appPreferencesStore.isDeveloperModeEnabledFlow().first() + return Command.entries + .asSequence() + .filter { it.startsWith(text) } + .filter { !isInThread || it.isAllowedInThread } + .filter { !it.isDevCommand || isDeveloperModeEnabled } + // Don't include the change display name commands if the user can't change their display name + .run { + val canUserChangeDisplayName = withTimeoutOrNull(5.seconds) { + capabilitiesProvider.canChangeDisplayName().getOrNull() + } ?: false + if (!canUserChangeDisplayName) { + filterNot { it == Command.CHANGE_DISPLAY_NAME || it == Command.CHANGE_DISPLAY_NAME_FOR_ROOM } + } else { + this + } + } + // Don't include the change avatar commands if the user can't change their avatar url + .run { + val canUserChangeAvatar = withTimeoutOrNull(5.seconds) { + capabilitiesProvider.canChangeAvatarUrl().getOrNull() + } ?: false + if (!canUserChangeAvatar) { + filterNot { it == Command.CHANGE_AVATAR || it == Command.CHANGE_AVATAR_FOR_ROOM } + } else { + this + } + } + .map { + SlashCommandSuggestion( + command = it.command, + parameters = it.parameters, + description = stringProvider.getString(it.description), + ) + } + .toList() + } + + override suspend fun parse( + textMessage: CharSequence, + formattedMessage: String?, + isInThreadTimeline: Boolean, + ): SlashCommand { + return commandParser.parseSlashCommand( + textMessage = textMessage, + formattedMessage = formattedMessage, + isInThreadTimeline = isInThreadTimeline, + ) + } + + override suspend fun proceedSendMessage( + slashCommand: SlashCommand.SlashCommandSendMessage, + timeline: Timeline, + ): Result { + return commandExecutor.proceedSendMessage( + slashCommand = slashCommand, + timeline = timeline, + ) + } + + override suspend fun proceedAdmin( + slashCommand: SlashCommand.SlashCommandAdmin, + ): Result { + return commandExecutor.proceedAdmin( + slashCommand = slashCommand, + ) + } +} diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/rainbow/RainbowGenerator.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/rainbow/RainbowGenerator.kt new file mode 100644 index 0000000000..594b51cbf6 --- /dev/null +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/rainbow/RainbowGenerator.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl.rainbow + +import dev.zacsweers.metro.Inject +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sin + +/** + * Inspired from React-Sdk + * Ref: https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/colour.js + */ +@Inject +class RainbowGenerator { + fun generate(text: String): String { + val split = text.splitEmoji() + val frequency = 2 * Math.PI / split.size + + return split + .mapIndexed { idx, letter -> + // Do better than React-Sdk: Avoid adding font color for spaces + if (letter == " ") { + "$letter" + } else { + val (a, b) = generateAB(idx * frequency, 1f) + val dashColor = labToRGB(75, a, b).toDashColor() + "$letter" + } + } + .joinToString(separator = "") + } + + private fun generateAB(hue: Double, chroma: Float): Pair { + val a = chroma * 127 * cos(hue) + val b = chroma * 127 * sin(hue) + + return Pair(a, b) + } + + private fun labToRGB(l: Int, a: Double, b: Double): RgbColor { + // Convert CIELAB to CIEXYZ (D65) + var y = (l + 16) / 116.0 + val x = adjustXYZ(y + a / 500) * 0.9505 + val z = adjustXYZ(y - b / 200) * 1.0890 + + y = adjustXYZ(y) + + // Linear transformation from CIEXYZ to RGB + val red = 3.24096994 * x - 1.53738318 * y - 0.49861076 * z + val green = -0.96924364 * x + 1.8759675 * y + 0.04155506 * z + val blue = 0.05563008 * x - 0.20397696 * y + 1.05697151 * z + + return RgbColor(adjustRGB(red), adjustRGB(green), adjustRGB(blue)) + } + + private fun adjustXYZ(value: Double): Double { + if (value > 0.2069) { + return value.pow(3) + } + return 0.1284 * value - 0.01771 + } + + private fun gammaCorrection(value: Double): Double { + // Non-linear transformation to sRGB + if (value <= 0.0031308) { + return 12.92 * value + } + return 1.055 * value.pow(1 / 2.4) - 0.055 + } + + private fun adjustRGB(value: Double): Int { + return (gammaCorrection(value) + .coerceIn(0.0, 1.0) * 255) + .roundToInt() + } +} + +/** + * Same as split, but considering emojis. + */ +private fun CharSequence.splitEmoji(): List { + val result = mutableListOf() + var index = 0 + while (index < length) { + val firstChar = get(index) + if (firstChar.code == 0x200e) { + // Left to right mark. What should I do with it? + } else if (firstChar.code in 0xD800..0xDBFF && index + 1 < length) { + // We have the start of a surrogate pair + val secondChar = get(index + 1) + if (secondChar.code in 0xDC00..0xDFFF) { + // We have an emoji + result.add("$firstChar$secondChar") + index++ + } else { + // Not sure what we have here... + result.add("$firstChar") + } + } else { + // Regular char + result.add("$firstChar") + } + index++ + } + return result +} diff --git a/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/rainbow/RgbColor.kt b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/rainbow/RgbColor.kt new file mode 100644 index 0000000000..c425d81d73 --- /dev/null +++ b/libraries/slashcommands/impl/src/main/kotlin/io/element/android/libraries/slashcommands/impl/rainbow/RgbColor.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl.rainbow + +data class RgbColor( + val r: Int, + val g: Int, + val b: Int +) + +fun RgbColor.toDashColor(): String { + return listOf(r, g, b) + .joinToString(separator = "", prefix = "#") { + it.toString(16).padStart(2, '0') + } +} diff --git a/libraries/slashcommands/impl/src/main/res/values/temporary.xml b/libraries/slashcommands/impl/src/main/res/values/temporary.xml new file mode 100644 index 0000000000..26232ea9b3 --- /dev/null +++ b/libraries/slashcommands/impl/src/main/res/values/temporary.xml @@ -0,0 +1,48 @@ + + + Command error + Unrecognized command: %1$s + The command \"%1$s\" needs more parameters, or some parameters are incorrect.The syntax is\n\n%2$s + The command \"%1$s\" is recognized but not supported in threads. + Displays action + Crash the application. + Bans user with given id + Unbans user with given id + Ignores a user, hiding their messages from you + Stops ignoring a user, showing their messages going forward + Define the power level of a user + Deops user with given id + Sets the room name + Sends the given message colored as a rainbow + Sends the given emote colored as a rainbow + Invites user with given id to current room + Joins room with given address + Sends the given message as a spoiler + Set the room topic + Removes user with given id from this room + Changes your display nickname + Changes your profile picture in all rooms + Sends the given message with confetti + Sends the given message with snowfall + Sends a message as plain text, without interpreting it as markdown + Changes your display nickname in the current room only + Changes the avatar of the current room + Changes your avatar in this current room only + Open the developer tools screen + Displays information about a user + Prepends ¯\\_(ツ)_/¯ to a plain-text message + Prepends ( ͡° ͜ʖ ͡°) to a plain-text message + Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message + Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message + Forces the current outbound group session in an encrypted room to be discarded + Only supported in encrypted rooms + Leave the current room + Upgrades a room to a new version + + Spoiler + diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt new file mode 100644 index 0000000000..497f45c96f --- /dev/null +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandExecutorTest.kt @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.timeline.MsgType +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.slashcommands.api.ChatEffect +import io.element.android.libraries.slashcommands.api.MessagePrefix +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.impl.rainbow.RainbowGenerator +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CommandExecutorTest { + @Test + fun `send plain text delegates to timeline with plain flag`() = runTest { + val timeline = FakeTimeline() + var capturedBody: String? = null + var capturedHtml: String? = "initial" + var capturedAsPlainText = false + timeline.sendMessageLambda = { body, htmlBody, _, _, asPlainText -> + capturedBody = body + capturedHtml = htmlBody + capturedAsPlainText = asPlainText + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendPlainText("hello"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("hello") + assertThat(capturedHtml).isNull() + assertThat(capturedAsPlainText).isTrue() + } + + @Test + fun `send emote delegates to timeline as emote`() = runTest { + val timeline = FakeTimeline() + var msgType: MsgType? = null + timeline.sendMessageLambda = { _, _, _, type, _ -> + msgType = type + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendEmote("yay"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(msgType).isEqualTo(MsgType.MSG_TYPE_EMOTE) + } + + @Test + fun `send lenny prefixes message`() = runTest { + val timeline = FakeTimeline() + var capturedBody: String? = null + timeline.sendMessageLambda = { body, _, _, _, _ -> + capturedBody = body + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendWithPrefix(MessagePrefix.Lenny, "fun"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("( ͡° ͜ʖ ͡°) fun") + } + + @Test + fun `send table flip prefixes message`() = runTest { + val timeline = FakeTimeline() + var capturedBody: String? = null + timeline.sendMessageLambda = { body, _, _, _, _ -> + capturedBody = body + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendWithPrefix(MessagePrefix.TableFlip, "wow"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("(╯°□°)╯︵ ┻━┻ wow") + } + + @Test + fun `send unflip prefixes message`() = runTest { + val timeline = FakeTimeline() + var capturedBody: String? = null + timeline.sendMessageLambda = { body, _, _, _, _ -> + capturedBody = body + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendWithPrefix(MessagePrefix.Unflip, "keep cool"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("┬──┬ ノ( ゜-゜ノ) keep cool") + } + + @Test + fun `send shrug prefixes message`() = runTest { + val timeline = FakeTimeline() + var capturedBody: String? = null + timeline.sendMessageLambda = { body, _, _, _, _ -> + capturedBody = body + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendWithPrefix(MessagePrefix.Shrug, "wow"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("¯\\\\_(ツ)\\_/¯ wow") + } + + @Test + fun `send rainbow provides html body`() = runTest { + val timeline = FakeTimeline() + var capturedHtml: String? = null + var capturedBody: String? = null + var capturedMsgType: MsgType? = null + timeline.sendMessageLambda = { body, htmlBody, _, msgType, _ -> + capturedBody = body + capturedHtml = htmlBody + capturedMsgType = msgType + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendRainbow("a nice rainbow"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("a nice rainbow") + assertThat(capturedHtml).isNotNull() + assertThat(capturedHtml!!.contains(" + capturedBody = body + capturedHtml = htmlBody + capturedMsgType = msgType + Result.success(Unit) + } + val sut = createCommandExecutor() + val res = sut.proceedSendMessage(SlashCommand.SendRainbowEmote("a nice rainbow"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("a nice rainbow") + assertThat(capturedHtml).isNotNull() + assertThat(capturedHtml!!.contains(" + capturedBody = body + capturedHtml = htmlBody + Result.success(Unit) + } + val stringProvider = FakeStringProvider(defaultResult = "SPOILER") + val sut = createCommandExecutor( + stringProvider = stringProvider, + ) + val res = sut.proceedSendMessage(SlashCommand.SendSpoiler("secret"), timeline) + assertThat(res.isSuccess).isTrue() + assertThat(capturedBody).isEqualTo("[SPOILER](secret)") + assertThat(capturedHtml).isEqualTo("secret") + } + + @Test + fun `send chat effect is not supported`() = runTest { + val sut = createCommandExecutor() + val res = sut.proceedSendMessage( + SlashCommand.SendChatEffect(ChatEffect.CONFETTI, A_MESSAGE), + FakeTimeline() + ) + assertThat(res.isFailure).isTrue() + } + + @Test + fun `admin commands call underlying client and room APIs`() = runTest { + var kicked = false + var banned = false + var unbanned = false + var invited = false + var ignored = false + var unignored = false + var left = false + var topicSet = false + var nameSet = false + var joined = false + + val joinedRoom = FakeJoinedRoom( + kickUserResult = { _, _ -> + kicked = true + Result.success(Unit) + }, + banUserResult = { _, _ -> + banned = true + Result.success(Unit) + }, + unBanUserResult = { _, _ -> + unbanned = true + Result.success(Unit) + }, + inviteUserResult = { _ -> + invited = true + Result.success(Unit) + }, + setTopicResult = { _ -> + topicSet = true + Result.success(Unit) + }, + setNameResult = { _ -> + nameSet = true + Result.success(Unit) + }, + baseRoom = FakeBaseRoom( + leaveRoomLambda = { + left = true + Result.success(Unit) + }, + ) + ) + val matrixClient = FakeMatrixClient( + ignoreUserResult = { _ -> + ignored = true + Result.success(Unit) + }, + unIgnoreUserResult = { _ -> + unignored = true + Result.success(Unit) + }, + ).apply { + joinRoomByIdOrAliasLambda = { _, _ -> + joined = true + Result.success(null) + } + } + val sut = createCommandExecutor( + matrixClient = matrixClient, + joinedRoom = joinedRoom, + ) + val kickRes = sut.proceedAdmin(SlashCommand.RemoveUser(A_USER_ID, null)) + assertThat(kicked).isTrue() + assertThat(kickRes.isSuccess).isTrue() + val banRes = sut.proceedAdmin(SlashCommand.BanUser(A_USER_ID, "reason")) + assertThat(banned).isTrue() + assertThat(banRes.isSuccess).isTrue() + val unbanRes = sut.proceedAdmin(SlashCommand.UnbanUser(A_USER_ID, null)) + assertThat(unbanned).isTrue() + assertThat(unbanRes.isSuccess).isTrue() + val inviteRes = sut.proceedAdmin(SlashCommand.Invite(A_USER_ID, null)) + assertThat(invited).isTrue() + assertThat(inviteRes.isSuccess).isTrue() + val ignoreRes = sut.proceedAdmin(SlashCommand.IgnoreUser(A_USER_ID)) + assertThat(ignoreRes.isSuccess).isTrue() + assertThat(ignored).isTrue() + val unignoreRes = sut.proceedAdmin(SlashCommand.UnignoreUser(A_USER_ID)) + assertThat(unignoreRes.isSuccess).isTrue() + assertThat(unignored).isTrue() + val leaveRes = sut.proceedAdmin(SlashCommand.LeaveRoom) + assertThat(leaveRes.isSuccess).isTrue() + assertThat(left).isTrue() + val topicRes = sut.proceedAdmin(SlashCommand.ChangeTopic("t")) + assertThat(topicRes.isSuccess).isTrue() + assertThat(topicSet).isTrue() + val nameRes = sut.proceedAdmin(SlashCommand.ChangeRoomName("n")) + assertThat(nameRes.isSuccess).isTrue() + assertThat(nameSet).isTrue() + val joinRes = sut.proceedAdmin( + SlashCommand.JoinRoom( + roomIdOrAlias = RoomIdOrAlias.Id( + RoomId("!room:domain") + ), + reason = null, + ) + ) + assertThat(joinRes.isSuccess).isTrue() + assertThat(joined).isTrue() + } +} + +fun createCommandExecutor( + matrixClient: FakeMatrixClient = FakeMatrixClient(), + joinedRoom: FakeJoinedRoom = FakeJoinedRoom(), + rainbowGenerator: RainbowGenerator = RainbowGenerator(), + stringProvider: StringProvider = FakeStringProvider(), +) = CommandExecutor( + matrixClient = matrixClient, + joinedRoom = joinedRoom, + rainbowGenerator = rainbowGenerator, + stringProvider = stringProvider, +) diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt new file mode 100644 index 0000000000..f5a6f54dfd --- /dev/null +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/CommandParserTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.libraries.slashcommands.api.ChatEffect +import io.element.android.libraries.slashcommands.api.MessagePrefix +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CommandParserTest { + @Test + fun parseSlashCommandEmpty() = runTest { + test("/", SlashCommand.ErrorEmptySlashCommand("A string/")) + } + + @Test + fun parseSlashCommandUnknown() = runTest { + test("/unknown", SlashCommand.ErrorUnknownSlashCommand("A string/unknown")) + test("/unknown with param", SlashCommand.ErrorUnknownSlashCommand("A string/unknown")) + } + + @Test + fun parseSlashCommandNotACommand() = runTest { + test("", SlashCommand.NotACommand) + test("test", SlashCommand.NotACommand) + test("// test", SlashCommand.NotACommand) + } + + @Test + fun parseSlashCommandEmote() = runTest { + test("/me test", SlashCommand.SendEmote("test")) + test("/me", SlashCommand.ErrorSyntax("A string/me, /me ")) + } + + @Test + fun parseSlashCommandRemove() = runTest { + // Nominal + test("/remove $A_USER_ID", SlashCommand.RemoveUser(A_USER_ID, null)) + // With a reason + test("/remove $A_USER_ID a reason", SlashCommand.RemoveUser(A_USER_ID, "a reason")) + // Trim the reason + test("/remove $A_USER_ID a reason ", SlashCommand.RemoveUser(A_USER_ID, "a reason")) + // Alias + test("/kick $A_USER_ID", SlashCommand.RemoveUser(A_USER_ID, null)) + // Error + test("/remove", SlashCommand.ErrorSyntax("A string/remove, /remove [reason]")) + } + + @Test + fun parseSlashCommandRemoveMarkdown() = runTest { + // Nominal + test( + "/remove [@user:domain.org](https://matrix.to/#/@user:domain.org)", + SlashCommand.RemoveUser(UserId("@user:domain.org"), null) + ) + test( + "/remove [@user:domain.org](https://matrix.to/#/@user:domain.org) reason", + SlashCommand.RemoveUser(UserId("@user:domain.org"), "reason") + ) + } + + @Test + fun parseSlashCommandPlain() = runTest { + test("/plain hello", SlashCommand.SendPlainText("hello")) + test("/plain", SlashCommand.ErrorSyntax("A string/plain, /plain ")) + } + + @Test + fun parseSlashCommandNickAndMyAvatar() = runTest { + test("/nick John", SlashCommand.ChangeDisplayName("John")) + test("/nick", SlashCommand.ErrorSyntax("A string/nick, /nick ")) + + test("/myavatar mxc://matrix.org/abc", SlashCommand.ChangeAvatar("mxc://matrix.org/abc")) + test("/myavatar http://notmxc", SlashCommand.ErrorSyntax("A string/myavatar, /myavatar ")) + test("/myavatar", SlashCommand.ErrorSyntax("A string/myavatar, /myavatar ")) + } + + @Test + fun parseSlashCommandRoomNickAndAvatars() = runTest { + test("/myroomnick Roomy", SlashCommand.ChangeDisplayNameForRoom("Roomy")) + test("/roomavatar mxc://matrix.org/abc", SlashCommand.ChangeRoomAvatar("mxc://matrix.org/abc")) + test("/roomavatar http://notmxc", SlashCommand.ErrorSyntax("A string/roomavatar, /roomavatar ")) + test("/myroomavatar mxc://matrix.org/abc", SlashCommand.ChangeAvatarForRoom("mxc://matrix.org/abc")) + } + + @Test + fun parseSlashCommandTopicAndRainbow() = runTest { + test("/topic New topic", SlashCommand.ChangeTopic("New topic")) + test("/topic", SlashCommand.ErrorSyntax("A string/topic, /topic ")) + + test("/rainbow yay", SlashCommand.SendRainbow("yay")) + test("/rainbow", SlashCommand.ErrorSyntax("A string/rainbow, /rainbow ")) + + test("/rainbowme yay", SlashCommand.SendRainbowEmote("yay")) + test("/rainbowme", SlashCommand.ErrorSyntax("A string/rainbowme, /rainbowme ")) + } + + @Test + fun parseSlashCommandJoinAndRoomName() = runTest { + // valid join + test( + "/join !roomId:domain reason", + SlashCommand.JoinRoom( + RoomIdOrAlias.Id(RoomId("!roomId:domain")), + "reason" + ) + ) + + // invalid join + test("/join notavalid", SlashCommand.ErrorSyntax("A string/join, /join [reason]")) + + test("/roomname My Room", SlashCommand.ChangeRoomName("My Room")) + test("/roomname", SlashCommand.ErrorSyntax("A string/roomname, /roomname ")) + } + + @Test + fun parseSlashCommandInviteBanEtc() = runTest { + test("/invite $A_USER_ID", SlashCommand.Invite(A_USER_ID, null)) + test("/invite", SlashCommand.ErrorSyntax("A string/invite, /invite [reason]")) + + test("/ban $A_USER_ID bad", SlashCommand.BanUser(A_USER_ID, "bad")) + test("/unban $A_USER_ID", SlashCommand.UnbanUser(A_USER_ID, null)) + + test("/ignore $A_USER_ID", SlashCommand.IgnoreUser(A_USER_ID)) + test("/unignore $A_USER_ID", SlashCommand.UnignoreUser(A_USER_ID)) + } + + @Test + fun parseSlashCommandPowerLevels() = runTest { + test("/op $A_USER_ID 50", SlashCommand.SetUserPowerLevel(A_USER_ID, 50)) + test("/op $A_USER_ID notnumber", SlashCommand.ErrorSyntax("A string/op, /op []")) + test("/deop $A_USER_ID", SlashCommand.SetUserPowerLevel(A_USER_ID, null)) + } + + @Test + fun parseSlashCommandDevtoolsAndSpoiler() = runTest { + test("/devtools", SlashCommand.DevTools) + test("/devtools extra", SlashCommand.ErrorSyntax("A string/devtools, /devtools")) + + test("/spoiler secret", SlashCommand.SendSpoiler("secret")) + test("/spoiler", SlashCommand.ErrorSyntax("A string/spoiler, /spoiler ")) + } + + @Test + fun parseSlashCommandEmojisAndSession() = runTest { + test("/shrug hello", SlashCommand.SendWithPrefix(MessagePrefix.Shrug, "hello")) + test("/shrug", SlashCommand.SendWithPrefix(MessagePrefix.Shrug, "")) + + test("/lenny fun", SlashCommand.SendWithPrefix(MessagePrefix.Lenny, "fun")) + test("/tableflip wow", SlashCommand.SendWithPrefix(MessagePrefix.TableFlip, "wow")) + test("/unflip be safe", SlashCommand.SendWithPrefix(MessagePrefix.Unflip, "be safe")) + + test("/discardsession", SlashCommand.DiscardSession) + test("/discardsession extra", SlashCommand.ErrorSyntax("A string/discardsession, /discardsession")) + } + + @Test + fun parseSlashCommandWhoisAndEffectsAndLeave() = runTest { + test("/whois $A_USER_ID", SlashCommand.ShowUser(A_USER_ID)) + + test("/confetti party", SlashCommand.SendChatEffect(ChatEffect.CONFETTI, "party")) + test("/snowfall snow", SlashCommand.SendChatEffect(ChatEffect.SNOWFALL, "snow")) + + test("/leave", SlashCommand.LeaveRoom) + test("/leave now", SlashCommand.ErrorSyntax("A string/leave, /leave")) + } + + @Test + fun parseSlashCommandUpgradeAndCrashAndFeatureFlagAndThreads() = runTest { + test("/upgraderoom 9", SlashCommand.UpgradeRoom("9")) + test("/upgraderoom", SlashCommand.ErrorSyntax("A string/upgraderoom, /upgraderoom newVersion")) + + // Crash only when developer mode enabled + val cpDev = createCommandParser(appPreferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = true)) + try { + cpDev.parseSlashCommand("/crash", null, false) + org.junit.Assert.fail("Expected crash to throw") + } catch (_: IllegalStateException) { + // expected + } + + // Feature flag disabled + val cpFF = createCommandParser(featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.SlashCommand.key to false))) + val res = cpFF.parseSlashCommand("/me test", null, false) + assertThat(res).isEqualTo(SlashCommand.NotACommand) + + // Not supported in threads (e.g. /join) + val cpThread = createCommandParser() + val threadRes = cpThread.parseSlashCommand("/join !roomId:domain", null, true) + assertThat(threadRes).isInstanceOf(SlashCommand.ErrorCommandNotSupportedInThreads::class.java) + assertThat((threadRes as SlashCommand.ErrorCommandNotSupportedInThreads).message).isEqualTo("A string/join") + } + + private suspend fun test(message: String, expectedResult: SlashCommand) { + val commandParser = createCommandParser() + val result = commandParser.parseSlashCommand(message, null, false) + assertThat(result).isEqualTo(expectedResult) + } +} + +internal fun createCommandParser( + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SlashCommand.key to true, + ), + ), + stringProvider: StringProvider = FakeStringProvider(), +) = CommandParser( + appPreferencesStore = appPreferencesStore, + featureFlagService = featureFlagService, + stringProvider = stringProvider, +) diff --git a/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt new file mode 100644 index 0000000000..cee4d17b21 --- /dev/null +++ b/libraries/slashcommands/impl/src/test/kotlin/io/element/android/libraries/slashcommands/impl/DefaultSlashCommandServiceTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.room.IntentionalMention +import io.element.android.libraries.matrix.api.timeline.MsgType +import io.element.android.libraries.matrix.test.FakeHomeserverCapabilitiesProvider +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeBaseRoom +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.impl.rainbow.RainbowGenerator +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultSlashCommandServiceTest { + @Test + fun `getSuggestions filters by text and maps to suggestions`() = runTest { + val stringProvider = FakeStringProvider(defaultResult = "desc") + val prefs = InMemoryAppPreferencesStore(isDeveloperModeEnabled = false) + val sut = createDefaultSlashCommandService( + commandParser = CommandParser( + appPreferencesStore = prefs, + featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SlashCommand.key to true, + ) + ), + stringProvider = stringProvider, + ), + stringProvider = stringProvider, + appPreferencesStore = prefs, + ) + val res = sut.getSuggestions("ra", isInThread = true) + // Expect commands starting with "/ra" (case-insensitive) and that are allowed in threads + assertThat(res).isNotEmpty() + assertThat(res.first().description).isEqualTo("desc") + } + + @Test + fun `getSuggestions hides dev commands when developer mode disabled`() = runTest { + val stringProvider = FakeStringProvider() + val prefs = InMemoryAppPreferencesStore(isDeveloperModeEnabled = false) + val sut = createDefaultSlashCommandService(appPreferencesStore = prefs, stringProvider = stringProvider) + val all = sut.getSuggestions("crash", isInThread = true) + assertThat(all).isEmpty() + } + + @Test + fun `getSuggestions returns empty list when the feature is enabled`() = runTest { + val sut = createDefaultSlashCommandService(isFeatureEnabled = true) + val all = sut.getSuggestions("me", isInThread = false) + assertThat(all).isNotEmpty() + } + + @Test + fun `getSuggestions returns empty list when the feature is disabled`() = runTest { + val sut = createDefaultSlashCommandService(isFeatureEnabled = false) + val all = sut.getSuggestions("me", isInThread = false) + assertThat(all).isEmpty() + } + + @Test + fun `getSuggestions for aliases`() = runTest { + val stringProvider = FakeStringProvider() + val prefs = InMemoryAppPreferencesStore(isDeveloperModeEnabled = false) + val sut = createDefaultSlashCommandService(appPreferencesStore = prefs, stringProvider = stringProvider) + val all = sut.getSuggestions("part", isInThread = true) + assertThat(all).isEmpty() + } + + @Test + fun `getSuggestions shows dev commands when developer mode enabled`() = runTest { + val stringProvider = FakeStringProvider() + val prefs = InMemoryAppPreferencesStore(isDeveloperModeEnabled = true) + val sut = createDefaultSlashCommandService(appPreferencesStore = prefs, stringProvider = stringProvider) + val all = sut.getSuggestions("crash", isInThread = true) + assertThat(all).isNotEmpty() + assertThat(all.first().command).isEqualTo("/crash") + } + + @Test + fun `parse delegates to commandParser`() = runTest { + val sut = createDefaultSlashCommandService() + val res = sut.parse("test", null, false) + assertThat(res).isEqualTo(SlashCommand.NotACommand) + } + + @Test + fun `proceedSendMessage delegate to commandExecutor`() = runTest { + val sendMessage = lambdaRecorder { _: String, _: String?, _: List, _: MsgType, _: Boolean -> + Result.success(Unit) + } + val sut = createDefaultSlashCommandService() + val sendRes = sut.proceedSendMessage( + slashCommand = SlashCommand.SendPlainText("hi"), + timeline = FakeTimeline().apply { + sendMessageLambda = sendMessage + }, + ) + assertThat(sendRes.isSuccess).isTrue() + sendMessage.assertions().isCalledOnce() + } + + @Test + fun `canChangeDisplayName is respected in suggestions`() = runTest { + var result = false + val capabilitiesProvider = FakeHomeserverCapabilitiesProvider( + canChangeDisplayName = { Result.success(result) }, + ) + val sut = createDefaultSlashCommandService(capabilitiesProvider = capabilitiesProvider) + + // Initially, with a disabled capability, the change display name command should not be in the suggestions + var changeNameCommand = sut.getSuggestions("", isInThread = false) + .find { it.command == Command.CHANGE_DISPLAY_NAME.command } + assertThat(changeNameCommand).isNull() + + // When the capability is true, the command should be included in the suggestions + result = true + changeNameCommand = sut.getSuggestions("", isInThread = false) + .find { it.command == Command.CHANGE_DISPLAY_NAME.command } + assertThat(changeNameCommand).isNotNull() + } + + @Test + fun `proceedAdmin delegates to commandExecutor`() = runTest { + val leaveRoomLambda = lambdaRecorder> { + Result.success(Unit) + } + val sut = createDefaultSlashCommandService( + commandExecutor = CommandExecutor( + matrixClient = FakeMatrixClient(), + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + leaveRoomLambda = leaveRoomLambda + ), + ), + rainbowGenerator = RainbowGenerator(), + stringProvider = FakeStringProvider(), + ), + ) + val adminRes = sut.proceedAdmin(SlashCommand.LeaveRoom) + assertThat(adminRes.isSuccess).isTrue() + leaveRoomLambda.assertions().isCalledOnce() + } + + private fun createDefaultSlashCommandService( + isFeatureEnabled: Boolean = true, + featureFlagService: FeatureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.SlashCommand.key to isFeatureEnabled, + ), + ), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + stringProvider: StringProvider = FakeStringProvider(), + commandParser: CommandParser = createCommandParser( + featureFlagService = featureFlagService, + appPreferencesStore = appPreferencesStore, + stringProvider = stringProvider, + ), + commandExecutor: CommandExecutor = createCommandExecutor( + stringProvider = stringProvider, + ), + capabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(), + ) = DefaultSlashCommandService( + commandParser = commandParser, + commandExecutor = commandExecutor, + stringProvider = stringProvider, + appPreferencesStore = appPreferencesStore, + featureFlagService = featureFlagService, + capabilitiesProvider = capabilitiesProvider, + ) +} diff --git a/libraries/slashcommands/test/build.gradle.kts b/libraries/slashcommands/test/build.gradle.kts new file mode 100644 index 0000000000..d8a54aa180 --- /dev/null +++ b/libraries/slashcommands/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.slashcommands.test" +} + +dependencies { + implementation(projects.libraries.slashcommands.api) + implementation(projects.libraries.matrix.api) + implementation(projects.tests.testutils) +} diff --git a/libraries/slashcommands/test/src/main/kotlin/io/element/android/libraries/slashcommands/test/FakeSlashCommandService.kt b/libraries/slashcommands/test/src/main/kotlin/io/element/android/libraries/slashcommands/test/FakeSlashCommandService.kt new file mode 100644 index 0000000000..319a8e647c --- /dev/null +++ b/libraries/slashcommands/test/src/main/kotlin/io/element/android/libraries/slashcommands/test/FakeSlashCommandService.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.slashcommands.test + +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.slashcommands.api.SlashCommand +import io.element.android.libraries.slashcommands.api.SlashCommandService +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask + +class FakeSlashCommandService( + private val getSuggestionsResult: (String, Boolean) -> List = { _, _ -> lambdaError() }, + private val parseResult: (CharSequence, String?, Boolean) -> SlashCommand = { _, _, _ -> lambdaError() }, + private val proceedSendMessageResult: (SlashCommand.SlashCommandSendMessage, Timeline) -> Result = { _, _ -> lambdaError() }, + private val proceedAdminResult: (SlashCommand.SlashCommandAdmin) -> Result = { lambdaError() }, +) : SlashCommandService { + override suspend fun getSuggestions(text: String, isInThread: Boolean): List = simulateLongTask { + getSuggestionsResult(text, isInThread) + } + + override suspend fun parse( + textMessage: CharSequence, + formattedMessage: String?, + isInThreadTimeline: Boolean, + ): SlashCommand = simulateLongTask { + parseResult(textMessage, formattedMessage, isInThreadTimeline) + } + + override suspend fun proceedSendMessage( + slashCommand: SlashCommand.SlashCommandSendMessage, + timeline: Timeline, + ): Result = simulateLongTask { + proceedSendMessageResult(slashCommand, timeline) + } + + override suspend fun proceedAdmin(slashCommand: SlashCommand.SlashCommandAdmin): Result = simulateLongTask { + proceedAdminResult(slashCommand) + } +} diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index a339890201..41e20582e0 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.testtags) implementation(projects.libraries.uiUtils) + implementation(projects.libraries.slashcommands.api) releaseApi(libs.matrix.richtexteditor) releaseApi(libs.matrix.richtexteditor.compose) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index 15aabdcbb0..08d89e25fb 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -134,7 +134,7 @@ private fun ReplyToModeView( modifier .clip(RoundedCornerShape(6.dp)) .background(ElementTheme.colors.bgCanvasDefault) - .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(6.dp)) + .border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp)) .padding(4.dp) ) { InReplyToView( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/ResolvedSuggestion.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/ResolvedSuggestion.kt index 22d73d9726..f9fd6a2b6d 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/ResolvedSuggestion.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/ResolvedSuggestion.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion @Immutable sealed interface ResolvedSuggestion { @@ -33,11 +34,7 @@ sealed interface ResolvedSuggestion { ) } - /** - * A slash command suggestion (e.g., /pay). - */ data class Command( - val command: String, - val description: String, + val command: SlashCommandSuggestion, ) : ResolvedSuggestion } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt index 588f87d821..6560bd8b69 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt @@ -61,21 +61,29 @@ class MarkdownTextEditorState( } is ResolvedSuggestion.Member -> { val currentText = SpannableStringBuilder(text.value()) - val mentionSpan = mentionSpanProvider.createUserMentionSpan(resolvedSuggestion.roomMember.userId) + val userId = resolvedSuggestion.roomMember.userId + val mentionSpan = mentionSpanProvider.createUserMentionSpan(userId) currentText.replace(suggestion.start, suggestion.end, "@ ") val end = suggestion.start + 1 currentText.setSpan(mentionSpan, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - this.text.update(currentText, true) - this.selection = IntRange(end + 1, end + 1) + text.update(currentText, true) + selection = IntRange(end + 1, end + 1) } is ResolvedSuggestion.Alias -> { val currentText = SpannableStringBuilder(text.value()) - val mentionSpan = mentionSpanProvider.createRoomMentionSpan(resolvedSuggestion.roomAlias.toRoomIdOrAlias()) + val roomAlias = resolvedSuggestion.roomAlias + val mentionSpan = mentionSpanProvider.createRoomMentionSpan(roomAlias.toRoomIdOrAlias()) currentText.replace(suggestion.start, suggestion.end, "# ") val end = suggestion.start + 1 currentText.setSpan(mentionSpan, suggestion.start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - this.text.update(currentText, true) - this.selection = IntRange(end + 1, end + 1) + text.update(currentText, true) + selection = IntRange(end + 1, end + 1) + } + is ResolvedSuggestion.Command -> { + // Just insert the command text + text.update("${resolvedSuggestion.command.command} ", true) + val length = resolvedSuggestion.command.command.length + 1 + selection = IntRange(length, length) } is ResolvedSuggestion.Command -> { // Insert the command text with a trailing space diff --git a/libraries/textcomposer/impl/src/main/res/values-ja/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..6dea90b306 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,33 @@ + + + "添付ファイルを追加" + "箇条リスト" + "書式設定を中止して閉じる" + "コードブロックを切替" + "キャプションを追加" + "暗号化されたメッセージ…" + "メッセージ…" + "暗号化されていないメッセージ…" + "リンクを作成" + "リンクを編集" + "%1$s の状態: %2$s" + "太字" + "斜体" + "無効" + "オフ" + "オン" + "取り消し線を追加" + "下線を追加" + "全画面モードの切替" + "インデント" + "コード部の書式" + "リンクを設定" + "番号リスト" + "記述設定を開く" + "引用の表示切替" + "リンクを削除" + "インデントを削除" + "リンク" + "古いアプリケーションを使用しているユーザーはキャプションを見られない可能性があります。" + "長押しで録音" + diff --git a/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml b/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..42b78d23f3 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,25 @@ + + + "Thêm tệp đính kèm" + "Chuyển đổi danh sách dấu đầu dòng" + "Hủy và đóng định dạng văn bản" + "Bật/tắt khối mã" + "Tin nhắn…" + "Tạo liên kết" + "Sửa liên kết" + "Áp dụng định dạng in đậm" + "Áp dụng định dạng in nghiêng" + "Áp dụng định dạng gạch ngang" + "Áp dụng định dạng gạch chân" + "Bật/tắt chế độ toàn màn hình" + "Thụt lề" + "Áp dụng định dạng mã trong dòng" + "Đặt liên kết" + "Chuyển đổi danh sách được đánh số" + "Mở tùy chọn soạn tin" + "Chuyển sang Trích dẫn" + "Xóa liên kết" + "Bỏ thụt lề" + "Liên kết" + "Nhấn giữ để ghi âm" + diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt index 04b700925e..6e57ce68cb 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/model/MarkdownTextEditorStateTest.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID 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.room.aRoomMember +import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.libraries.textcomposer.impl.mentions.aMentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpan import io.element.android.libraries.textcomposer.mentions.MentionType @@ -42,6 +43,7 @@ class MarkdownTextEditorStateTest { val mentionSpanProvider = aMentionSpanProvider() state.insertSuggestion(suggestion, mentionSpanProvider) assertThat(state.getMentions()).isEmpty() + assertThat(state.text.value().toString()).isEqualTo("Hello @") } @Test @@ -53,6 +55,7 @@ class MarkdownTextEditorStateTest { val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) state.insertSuggestion(suggestion, mentionSpanProvider) + assertThat(state.text.value().toString()).isEqualTo("Hello # ") } @Test @@ -64,6 +67,19 @@ class MarkdownTextEditorStateTest { val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) state.insertSuggestion(suggestion, mentionSpanProvider) + assertThat(state.text.value().toString()).isEqualTo("Hello # ") + } + + @Test + fun `insertSuggestion - command`() { + val state = aMarkdownTextEditorState(initialText = "/rai", initialFocus = true).apply { + currentSuggestion = Suggestion(start = 0, end = 3, type = SuggestionType.Command, text = "/rainbow") + } + val suggestion = aSlashCommandSuggestion() + val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) + val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) + state.insertSuggestion(suggestion, mentionSpanProvider) + assertThat(state.text.value().toString()).isEqualTo("/rainbow ") } @Test @@ -74,6 +90,7 @@ class MarkdownTextEditorStateTest { val mentionSpanProvider = aMentionSpanProvider() state.insertSuggestion(mention, mentionSpanProvider) assertThat(state.getMentions()).isEmpty() + assertThat(state.text.value().toString()).isEqualTo("Hello @") } @Test @@ -91,6 +108,7 @@ class MarkdownTextEditorStateTest { val mentions = state.getMentions() assertThat(mentions).isNotEmpty() assertThat((mentions.firstOrNull() as? IntentionalMention.User)?.userId).isEqualTo(member.userId) + assertThat(state.text.value().toString()).isEqualTo("Hello @ ") } @Test @@ -107,15 +125,14 @@ class MarkdownTextEditorStateTest { val mentions = state.getMentions() assertThat(mentions).isNotEmpty() assertThat(mentions.firstOrNull()).isInstanceOf(IntentionalMention.Room::class.java) + assertThat(state.text.value().toString()).isEqualTo("Hello @ ") } @Test fun `getMessageMarkdown - when there are no MentionSpans returns the same text`() { val text = "No mentions here" val state = aMarkdownTextEditorState(initialText = text, initialFocus = true) - val markdown = state.getMessageMarkdown(FakePermalinkBuilder()) - assertThat(markdown).isEqualTo(text) } @@ -128,19 +145,17 @@ class MarkdownTextEditorStateTest { ) val state = aMarkdownTextEditorState(initialText = text, initialFocus = true) state.text.update(aMarkdownTextWithMentions(), needsDisplaying = false) - val markdown = state.getMessageMarkdown(permalinkBuilder = permalinkBuilder) - assertThat(markdown).isEqualTo( "Hello [@alice:matrix.org](https://matrix.to/#/@alice:matrix.org) and everyone in @room" + " and a room [#room:domain.org](https://matrix.to/#/#room:domain.org)" ) + assertThat(state.text.value().toString()).isEqualTo("Hello @ and everyone in @ and a room #room:domain.org") } @Test fun `getMentions - when there are no MentionSpans returns empty list of mentions`() { val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true) - assertThat(state.getMentions()).isEmpty() } @@ -148,9 +163,7 @@ class MarkdownTextEditorStateTest { fun `getMentions - when there are MentionSpans returns a list of mentions`() { val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true) state.text.update(aMarkdownTextWithMentions(), needsDisplaying = false) - val mentions = state.getMentions() - assertThat(mentions).isNotEmpty() assertThat((mentions.firstOrNull() as? IntentionalMention.User)?.userId?.value).isEqualTo("@alice:matrix.org") assertThat(mentions.lastOrNull()).isInstanceOf(IntentionalMention.Room::class.java) @@ -184,4 +197,14 @@ class MarkdownTextEditorStateTest { roomAvatarUrl = null ) } + + private fun aSlashCommandSuggestion(): ResolvedSuggestion.Command { + return ResolvedSuggestion.Command( + command = SlashCommandSuggestion( + command = "/rainbow", + parameters = "param", + description = "Make the text colorful 🌈", + ), + ) + } } diff --git a/libraries/troubleshoot/impl/src/main/res/values-ja/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..a7357d7aae --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ja/translations.xml @@ -0,0 +1,12 @@ + + + "プッシュ履歴" + "テストを実行" + "再度テスト" + "一部のテストで失敗しました。詳細を確認してください。" + "テストを実行することで、不安定な通知を生じさせる設定の問題を特定できます。" + "修正を試行" + "テストは問題なく完了しました。" + "通知のトラブルシューティング" + "一部のテストはあなたの操作が必要です。詳細を確認してください。" + diff --git a/libraries/troubleshoot/impl/src/main/res/values-lt/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-lt/translations.xml new file mode 100644 index 0000000000..2befbbf398 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-lt/translations.xml @@ -0,0 +1,5 @@ + + + "Vykdyti testus" + "Vykdyti testus dar kartą" + diff --git a/libraries/troubleshoot/impl/src/main/res/values-vi/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..977938afab --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-vi/translations.xml @@ -0,0 +1,6 @@ + + + "Chạy thử nghiệm" + "Chạy các bài kiểm tra để phát hiện vấn đề trong cấu hình có thể khiến thông báo không hoạt động như mong đợi." + "Khắc phục sự cố thông báo" + diff --git a/libraries/ui-strings/src/main/res/values-be/translations.xml b/libraries/ui-strings/src/main/res/values-be/translations.xml index 6e2374a5aa..4011474193 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -1,5 +1,6 @@ + "Дадаць рэакцыю: %1$s" "Адрас" "Аватар" "Выдаліць" @@ -8,15 +9,21 @@ "Уведзена %1$d лічбы" "Уведзена %1$d лічб" + "Змяніць аватар" + "Поўны адрас будзе %1$s" + "Дэталі шыфравання" "Схаваць пароль" "Далучыцца да выкліку" "Перайсці ўніз" "Толькі згадкі" "Гук адключаны" + "Новыя згадкі" + "Новыя паведамленні" "Старонка %1$d" "Паўза" "Поле PIN-кода" "Прайграць" + "Хуткасць прайгравання" "Апытанне" "Апытанне скончана" "QR-код" @@ -34,11 +41,14 @@ "Адправіць файлы" "Паказаць пароль" "Пазваніць" + "Аватар карыстальніка" "Меню карыстальніка" "Паглядзець падрабязнасці" "Запісаць галасавое паведамленне." "Спыніць запіс" + "Ваш аватар" "Прыняць" + "Дадаць подпіс" "Дадаць у хроніку" "Назад" "Званок" @@ -52,26 +62,36 @@ "Пацвердзіць пароль" "Працягнуць" "Капіраваць" + "Скапіяваць подпіс" "Скапіраваць спасылку" "Скапіраваць спасылку на паведамленне" + "Скапіяваць тэкст" "Стварыць" "Стварыце пакой" "Дэактываваць" "Дэактываваць уліковы запіс" "Адхіліць" + "Адхіліць і заблакіраваць" "Выдаліць апытанне" + "Зняць выбар з усіх" "Адключыць" "Адмяніць" "Aдхіліць" "Гатова" "Рэдагаваць" + "Рэдагаваць подпіс" "Рэдагаваць апытанне" "Уключыць" "Скончыць апытанне" "Увядзіце PIN-код" + "Даследаваць публічныя прасторы" + "Завяршыць" "Забылі пароль?" "Пераслаць" "Вярнуцца" + "Перайсці да роляў і дазволаў" + "Перайсці ў Налады" + "Ігнараваць" "Запрасіць" "Запрасіць карыстальнікаў" "Запрасіць карыстальнікаў у %1$s" @@ -82,6 +102,7 @@ "Пакінуць" "Пакінуць размову" "Пакінуць пакой" + "Пакінуць прастору" "Загрузіць больш" "Кіраванне ўліковым запісам" "Кіраванне прыладамі" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index f343571f1e..38d9ac3d8e 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -122,6 +122,7 @@ "Opustit prostor" "Načíst více" "Spravovat účet" + "Správa účtu a zařízení" "Spravovat zařízení" "Spravovat místnosti" "Zpráva" @@ -164,14 +165,15 @@ "Sdílet aktuální polohu" "Zobrazit" "Přihlásit se znovu" - "Odhlásit se" - "Přesto se odhlásit" + "Odebrat toto zařízení" + "Přesto toto zařízení odebrat" "Přeskočit" "Začít" "Zahájit chat" "Začít znovu" "Zahájit ověření" "Klepnutím načtete mapu" + "Zastavit" "Vyfotit" "Klepnutím zobrazíte možnosti" "Přeložit" @@ -192,6 +194,7 @@ "Pokročilá nastavení" "obrázek" "Analytika" + "Synchronizace oznámení…" "Opustili jste místnost" "Byli jste odhlášeni z relace" "Vzhled" @@ -225,6 +228,7 @@ "Prázdný soubor" "Šifrování" "Šifrování povoleno" + "Končí v %1$s" "Zadejte svůj PIN" "Chyba" "Došlo k chybě, nemusíte dostávat oznámení o nových zprávách. Vyřešte prosím problémy s oznámeními z nastavení. @@ -251,6 +255,8 @@ Důvod: %1$s." "Řádek zkopírován do schránky" "Odkaz zkopírován do schránky" "Připojit nové zařízení" + "Aktuální poloha" + "Sdílení aktuální polohy skončilo" "Načítání…" "Načítání dalších…" @@ -351,9 +357,10 @@ Důvod: %1$s." "Nastavení" "Sdílet prostor" "Noví členové vidí historii" + "Sdílená aktuální poloha" "Sdílená poloha" "Sdílený prostor" - "Odhlašování" + "Odebrání zařízení" "Něco se nepovedlo" "Narazili jsme na problém. Zkuste to prosím znovu." "Prostor" @@ -374,12 +381,13 @@ Důvod: %1$s." "Text" "Oznámení třetích stran" "Vlákno" + "Vlákna" "Téma" "O čem je tato místnost?" "Nelze dešifrovat" "Šifrováno nezabezpečeným zařízením" "Nemáte přístup k této zprávě" - "Ověřená identita odesílatele se změnila" + "Ověřená digitální identita odesílatele byla resetována" "Pozvánky nebylo možné odeslat jednomu nebo více uživatelům." "Nelze odeslat pozvánky" "Odemknout" @@ -404,16 +412,17 @@ Důvod: %1$s." "Hlasová zpráva" "Čekání…" "Čekání na dešifrovací klíč" + "Čekání na aktuální polohu…" "Kdokoli může vidět historii" "Vy" "%1$s (%2$s) sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti." "%1$s sdílel(a) tuto zprávu v době, kdy jste nebyli v místnosti." "Tato místnost byla nastavena tak, aby noví členové mohli číst historii. %1$s" - "Identita uživatele %1$s se změnila. %2$s" - "Identita uživatele %1$s %2$s se změnila. %3$s" + "Identita uživatele %1$s byla resetována. %2$s" + "Identita uživatele %1$s %2$s byla resetována. %3$s" "(%1$s)" - "Identita uživatele %1$s se změnila." - "Identita uživatele %1$s %2$s se změnila. %3$s" + "Identita uživatele %1$s byla resetována." + "Identita uživatele %1$s %2$s byla resetována. %3$s" "Zrušit ověření" "Povolit přístup" "Odkaz %1$s vás přesměruje na jinou stránku %2$s @@ -445,6 +454,7 @@ Opravdu chcete pokračovat?" "%1$s nemá přístup k vaší poloze. Zkuste to prosím později." "Nepodařilo se nahrát hlasovou zprávu." "Místnost již neexistuje nebo pozvánka již není platná." + "Pro přístup k funkcím založeným na poloze prosím povolte GPS." "Zpráva nebyla nalezena" "%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení." "%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže." @@ -473,11 +483,11 @@ Opravdu chcete pokračovat?" "%1$d Připnutých zpráv" "Připnuté zprávy" - "Chystáte se přejít na svůj %1$s účet a obnovit svou identitu. Poté budete přesměrováni zpět do aplikace." - "Nemůžete to potvrdit? Přejděte na svůj účet a resetujte svou identitu." + "Chystáte se přejít na svůj účet %1$s, abyste resetovali svou digitální identitu. Poté budete přesměrováni zpět do aplikace." + "Nemůžete to potvrdit? Přejděte do svého účtu a resetujte svou digitální identitu." "Zrušit ověření a odeslat" "Ověření můžete zrušit a přesto odeslat tuto zprávu, nebo můžete prozatím zrušit a zkusit to znovu později po opětovném ověření %1$s." - "Vaše zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila" + "Vaše zpráva nebyla odeslána, protože byla resetována ověřená digitální identita %1$s" "Přesto odeslat zprávu" "%1$s používá jedno nebo více neověřených zařízení. Zprávu můžete přesto odeslat, nebo můžete prozatím zrušit a zkusit to znovu později poté, co %2$s ověří všechna svá zařízení." "Vaše zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení" @@ -509,7 +519,7 @@ Opravdu chcete pokračovat?" "Prostory" "Sdíleno %1$s" "Na mapě" - "Zpráva nebyla odeslána, protože ověřená identita uživatele %1$s se změnila." + "Zpráva nebyla odeslána, protože byla resetována ověřená digitální identita %1$s." "Zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení." "Zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení." "Poloha" @@ -519,5 +529,5 @@ Opravdu chcete pokračovat?" "Pro přístup k historickým zprávám musíte toto zařízení ověřit" "Nemáte přístup k této zprávě" "Nelze dešifrovat zprávu" - "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel potřebuje ověřit vaši identitu." + "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel musí ověřit vaši digitální identitu." diff --git a/libraries/ui-strings/src/main/res/values-da/translations.xml b/libraries/ui-strings/src/main/res/values-da/translations.xml index 5d1a08a727..e12e049469 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -120,6 +120,7 @@ "Forlad klynge" "Indlæs mere" "Administrer konto" + "Administrer konto og enheder" "Administrer enheder" "Administrer rum" "Besked" @@ -162,14 +163,15 @@ "Del liveplacering" "Vis" "Log ind igen" - "Log ud" - "Log ud alligevel" + "Fjern denne enhed" + "Fjern denne enhed alligevel" "Spring over" "Start" "Start samtale" "Begynd forfra" "Begynd verifikation" "Tryk for at indlæse kort" + "Stop" "Tag billede" "Tryk for indstillinger" "Oversæt" @@ -190,6 +192,7 @@ "Avancerede indstillinger" "et billede" "Analyse-værktøj" + "Synkroniserer notifikationer…" "Du forlod rummet" "Du blev logget ud af sessionen" "Udseende" @@ -223,6 +226,7 @@ "Tom fil" "Kryptering" "Kryptering aktiveret" + "Slutter kl.%1$s" "Indtast din PIN-kode" "Fejl" "Der opstod en fejl, du modtager muligvis ikke meddelelser om nye meddelelser. Fejlfinding af meddelelser fra indstillingerne. @@ -249,6 +253,8 @@ "Linje kopieret til udklipsholder" "Linket er kopieret til udklipsholderen" "Forbind ny enhed" + "Aktuel position" + "Aktuel position afsluttet" "Indlæser…" "Indlæser flere…" @@ -344,9 +350,10 @@ "Indstillinger" "Del klynge" "Nye medlemmer ser historik" + "Deling af aktuel position" "Delt placering" "Delt klynge" - "Logger ud" + "Fjerner enhed" "Noget gik galt" "Vi stødte på et problem. Prøv venligst igen." "Klynge" @@ -366,12 +373,13 @@ "Tekst" "Tredjepartsmeddelelser" "Tråd" + "Tråde" "Emne" "Hvad handler det her rum om?" "Ude af stand til at dekryptere" "Sendt fra en usikker enhed" "Du har ikke adgang til denne meddelelse" - "Afsenderens verificerede identitet blev nulstillet" + "Afsenderens verificerede digitale identitet blev nulstillet" "Invitationer kunne ikke sendes til en eller flere brugere." "Kan ikke sende invitation(er)" "Lås op" @@ -396,16 +404,17 @@ "Talebesked" "Venter…" "Venter på denne besked" + "Venter på aktuel position…" "Alle kan se historikken" "Dig" "%1$s(%2$s ) har delt denne besked siden du ikke var i rummet da den blev sendt." "%1$s delte denne besked, siden du ikke var i rummet da den blev sendt." "Dette rum er konfigureret, så nye medlemmer kan læse historikken.%1$s" - "%1$ss identitet blev nulstillet. %2$s" - "%1$ss %2$s identitet blev nulstillet. %3$s" + "%1$ss digitale identitet blev nulstillet. %2$s" + "%1$ss %2$s digitale identitet blev nulstillet. %3$s" "(%1$s)" - "%1$ss identitet blev nulstillet." - "%1$ss %2$s identitet blev nulstillet. %3$s" + "%1$ss digitale identitet blev nulstillet." + "%1$ss %2$s digitale identitet blev nulstillet. %3$s" "Tilbagetræk verifikation" "Tillad adgang" "Linket %1$s fører dig til et andet websted %2$s @@ -437,6 +446,7 @@ Er du sikker på, at du vil fortsætte?" "%1$s kunne ikke få adgang til din placering. Prøv igen senere." "Kunne ikke uploade din talebesked." "Rummet findes ikke længere, eller invitationen er ikke længere gyldig." + "Aktiver venligst din GPS for at få adgang til lokationsbaserede funktioner." "Meddelelsen blev ikke fundet" "%1$s har ikke tilladelse til at få adgang til din placering. Du kan aktivere adgang i Indstillinger." "%1$s har ikke tilladelse til at se din placering. Aktivér adgang nedenfor." @@ -464,11 +474,11 @@ Er du sikker på, at du vil fortsætte?" "%1$d Fastgjorte beskeder" "Fastgjorte beskeder" - "Du er ved at gå til din %1$s konto for at nulstille din identitet. Derefter vil du blive ført tilbage til appen." - "Kan du ikke bekræfte? Gå til din konto for at nulstille din identitet." + "Du er ved at gå til din %1$s konto for at nulstille din digitale identitet. Derefter vil du blive ført tilbage til appen." + "Kan du ikke bekræfte? Gå til din konto for at nulstille din digitale identitet." "Træk verifikationen tilbage og send" "Du kan trække din verifikation tilbage og sende denne meddelelse alligevel, eller du kan annullere for nu og prøve igen senere efter at have gen-verificeret. %1$s" - "Din besked blev ikke sendt, fordi %1$s\'s verificerede identitet er blevet nulstillet" + "Din besked blev ikke sendt, fordi %1$s\'s verificerede digitale identitet er blevet nulstillet" "Send besked alligevel" "%1$s bruger en eller flere uverificerede enheder. Du kan sende beskeden alligevel, eller du kan annullere for nu og prøve igen senere, når %2$s har bekræftet alle deres enheder." "Din besked blev ikke sendt, fordi %1$s ikke har bekræftet alle enheder" @@ -500,7 +510,7 @@ Er du sikker på, at du vil fortsætte?" "Klynger" "Delt %1$s" "På kortet" - "Beskeden blev ikke sendt fordi %1$s s bekræftede identitet blev nulstillet." + "Beskeden blev ikke sendt fordi %1$s s bekræftede digitale identitet blev nulstillet." "Meddelelsen er ikke sendt, fordi %1$s ikke har bekræftet alle enheder." "Beskeden er ikke sendt, fordi du ikke har verificeret en eller flere af dine enheder." "Lokation" @@ -510,5 +520,5 @@ Er du sikker på, at du vil fortsætte?" "Du skal verificere denne enhed for at få adgang til historiske beskeder" "Du har ikke adgang til denne meddelelse" "Kan ikke dekryptere beskeden" - "Denne besked blev blokeret, enten fordi du ikke verificerede din enhed, eller fordi afsenderen skal have verificeret din identitet." + "Denne besked blev blokeret, enten fordi du ikke verificerede din enhed, eller fordi afsenderen endnu ikke har bekræftet din digitale identitet." diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index 182f403140..7be84a8716 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -374,6 +374,7 @@ Syy: %1$s." "Teksti" "Kolmannen osapuolen ilmoitukset" "Viestiketju" + "Viestiketjut" "Aihe" "Mistä tässä huoneessa on kyse?" "Salauksen purkaminen ei onnistunut" @@ -466,6 +467,12 @@ Haluatko varmasti jatkaa?" "Poista %1$s" "Asetukset" "Median valinta epäonnistui, yritä uudelleen." + "Avaa Element Classic" + "Avaa Element Classic laitteellasi" + "Mene kohtaan \"Asetukset\" > \"Tietoturva ja yksityisyys\"" + "Osiossa \"Salausavainten hallinta\", paina \"Salattujen viestien palautus\"." + "Noudata ohjeita" + "Palaa takaisin %1$s -sovellukseen" "Tervetuloa takaisin" "Paina viestiä ja valitse “%1$s” lisätäksesi sen tänne." "Kiinnitä tärkeät viestit, jotta ne löytyvät helposti." diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 1dad21ff65..bec6d7f4ca 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -374,6 +374,7 @@ Raison : %1$s." "Texte" "Avis de tiers" "Fil de discussion" + "Fils de discussion" "Sujet" "De quoi s’agit-il dans ce salon ?" "Échec de déchiffrement" @@ -466,6 +467,13 @@ Raison : %1$s." "Supprimer %1$s" "Paramètres" "Échec de la sélection du média, veuillez réessayer." + "Ouvrir Element Classic" + "Ouvrez Element Classic sur votre appareil" + "Aller à Paramètres > Sécurité et vie privée" + "Dans Gestion des clés cryptographiques, sélectionnez Récupération des messages chiffrés" + "Suivez les instructions pour activer votre stockage de clés" + "Revenez à %1$s" + "Activez le stockage de vos clés avant de continuer avec %1$s" "Bon retour parmi nous" "Cliquez (clic long) sur un message et choisissez « %1$s » pour qu‘il apparaisse ici." "Épinglez les messages importants pour leur donner plus de visibilité" diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index bc4535dda1..9586b392e9 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -120,6 +120,7 @@ "Tér elhagyása" "Továbbiak betöltése" "Fiók kezelése" + "Fiók és eszközök kezelése" "Eszközök kezelése" "Szobák kezelése" "Üzenet" @@ -162,14 +163,15 @@ "Valós idejű hely megosztása" "Megjelenítés" "Jelentkezzen be újra" - "Kijelentkezés" - "Kijelentkezés mindenképp" + "Eszköz eltávolítása" + "Eszköz eltávolítása mindenképpen" "Kihagyás" "Indítás" "Csevegés indítása" "Újrakezdés" "Ellenőrzés elindítása" "Koppintson a térkép betöltéséhez" + "Leállítás" "Fénykép készítése" "Koppintson a beállításokért" "Fordítás" @@ -190,6 +192,7 @@ "Speciális beállítások" "egy kép" "Elemzések" + "Értesítések szinkronizálása…" "Elhagyta a szobát" "Ki lett jelentkeztetve a munkamenetből" "Megjelenítés" @@ -223,6 +226,7 @@ "Üres fájl" "Titkosítás" "Titkosítás engedélyezve" + "Vége: %1$s" "Adja meg a PIN-kódját" "Hiba" "Hiba történt, előfordulhat, hogy nem kap értesítést az új üzenetekről. Az értesítések hibaelhárítása a beállításokban található. @@ -249,6 +253,8 @@ Ok: %1$s." "A sor a vágólapra másolva" "Hivatkozás a vágólapra másolva" "Új eszköz összekapcsolása" + "Élő helymeghatározás" + "Élő pozíciómegosztás befejezve" "Betöltés…" "Továbbiak betöltése…" @@ -344,9 +350,10 @@ Ok: %1$s." "Beállítások" "Tér megosztása" "Az új tagok látják az előzményeket" + "Megosztott élő helymeghatározás" "Megosztott tartózkodási hely" "Megosztott tér" - "Kijelentkezés" + "Eszköz eltávolítása" "Valamilyen hiba történt" "Problémába ütköztünk. Próbálja újra." "Tér" @@ -366,6 +373,7 @@ Ok: %1$s." "Szöveg" "Harmadik felek nyilatkozatai" "Üzenetszál" + "Üzenetszálak" "Téma" "Miről szól ez a szoba?" "Nem lehet visszafejteni" @@ -396,6 +404,7 @@ Ok: %1$s." "Hangüzenet" "Várakozás…" "Várakozás erre az üzenetre" + "Várakozás az élő helymeghatározásra…" "Bárki láthatja az előzményeket" "Ön" "%1$s (%2$s) megosztotta ezt az üzenetet, mivel Ön nem volt a szobában, amikor elküldték." @@ -457,6 +466,13 @@ Biztos, hogy folytatja?" "Eltávolítás: %1$s" "Beállítások" "Nem sikerült kiválasztani a médiát, próbálja újra." + "Nyissa meg az Element Classic alkalmazást" + "Nyissa meg az Element Classic alkalmazást az eszközén" + "Lépjen a Beállítások > Biztonság és adatvédelem menüponthoz" + "A Kriptográfiai kulcsok kezelése részben válassza a Titkosított üzenetek helyreállítása lehetőséget" + "Kövesse az utasításokat a kulcstároló engedélyezéséhez" + "Térjen vissza ide: %1$s" + "Engedélyezze a kulcstárolást a folytatás előtt ide: %1$s" "Üdvözöljük újra!" "Nyomjon hosszan az üzenetre, és válassza a „%1$s” lehetőséget, hogy itt szerepeljen." "Tűzze ki a fontos üzeneteket, hogy könnyen felfedezhetők legyenek" diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 99c5db6077..0d73a8c121 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -1,6 +1,7 @@ "Aggiungi reazione: %1$s" + "Indirizzo" "Avatar" "Riduci al minimo il campo di testo del messaggio" "Elimina" @@ -26,9 +27,12 @@ "Pausa" "Messaggio vocale, durata: %1$s, posizione attuale: %2$s" "Campo del PIN" + "Posizione fissata" "Riproduci" + "Velocità di riproduzione" "Sondaggio" "Sondaggio terminato" + "Codice QR" "Reagisci con %1$s" "Reagisci con altri emoji" "Visualizzato da %1$s e %2$s" @@ -42,9 +46,12 @@ "Rimuovere la reazione con %1$s" "Avatar della stanza" "Invia file" + "Posizione del mittente" "Azione richiesta a tempo limitato, hai un minuto per la verifica" "Mostra password" "Avvia una chiamata" + "Avvia una videochiamata" + "Avvia una chiamata vocale" "Stanza obsoleta" "Avatar utente" "Menu utente" @@ -56,6 +63,7 @@ "Il tuo avatar" "Accetta" "Aggiungi didascalia" + "Aggiungi stanze esistenti" "Aggiungi alla conversazione" "Indietro" "Chiama" @@ -75,6 +83,7 @@ "Copia testo" "Crea" "Crea stanza" + "Crea spazio" "Disattiva" "Disattiva account" "Rifiuta" @@ -91,6 +100,7 @@ "Attiva" "Termina sondaggio" "Inserisci PIN" + "Esplora gli spazi pubblici" "Fine" "Password dimenticata?" "Inoltra" @@ -111,6 +121,7 @@ "Esci dallo spazio" "Carica altro" "Gestisci account" + "Gestisci account & dispositivi" "Gestisci dispositivi" "Gestisci le stanze" "Invia messaggio" @@ -150,18 +161,21 @@ "Invia messaggio vocale" "Condividi" "Condividi collegamento" + "Condividi la posizione in tempo reale" "Mostra" "Accedi di nuovo" - "Disconnetti" - "Disconnetti comunque" + "Rimuovi questo dispositivo" + "Rimuovi comunque questo dispositivo" "Salta" "Inizia" "Avvia conversazione" "Ricomincia" "Avvia la verifica" "Tocca per caricare la mappa" + "Ferma" "Scatta foto" "Tocca per le opzioni" + "Traduci" "Riprova" "Rimuovi dai fissati" "Visualizza" @@ -179,6 +193,7 @@ "Impostazioni avanzate" "un\'immagine" "Statistiche di utilizzo" + "Sincronizzazione delle notifiche…" "Hai lasciato la stanza" "Sei stato disconnesso dalla sessione" "Aspetto" @@ -191,6 +206,7 @@ "Copiato negli appunti" "Copyright" "Creazione stanza…" + "Creazione spazio…" "Richiesta annullata" "Hai lasciato la stanza" "Hai lasciato lo spazio" @@ -211,6 +227,7 @@ "File vuoto" "Crittografia" "Crittografia abilitata" + "Termina alle %1$s" "Inserisci il PIN" "Errore" "Si è verificato un errore, potresti non ricevere notifiche per nuovi messaggi. Risolvi i problemi relativi alle notifiche dalle impostazioni. @@ -236,6 +253,9 @@ Motivo:. %1$s" "Chiaro" "Riga copiata negli appunti" "Collegamento copiato negli appunti" + "Collega un nuovo dispositivo" + "Posizione in tempo reale" + "Posizione in tempo reale terminata" "Caricamento…" "Caricamento in corso…" @@ -248,10 +268,12 @@ Motivo:. %1$s" "Messaggio" "Azioni messaggio" + "Impossibile inviare il messaggio" "Impaginazione del messaggio" "Messaggio rimosso" "Moderno" "Silenzia" + "Nome" "%1$s (%2$s)" "Nessun risultato" "Nessun nome della stanza" @@ -260,6 +282,7 @@ Motivo:. %1$s" "Non in linea" "Licenze open source" "o" + "Altre opzioni" "Password" "Persone" "Collegamento permanente" @@ -277,8 +300,10 @@ Motivo:. %1$s" "Preparazione…" "Informativa sulla privacy" + "Privato" "Stanza privata" "Spazio privato" + "Pubblico" "Stanza pubblica" "Spazio pubblico" "Reazione" @@ -286,6 +311,7 @@ Motivo:. %1$s" "Motivo" "Chiave di recupero" "Aggiornamento…" + "Rimozione…" "%1$d risposta" "%1$d risposte" @@ -295,6 +321,7 @@ Motivo:. %1$s" "Segnala un problema" "Segnalazione inviata" "Editor di testo avanzato" + "Ruolo" "Stanza" "Nome stanza" "ad es. il nome del tuo progetto" @@ -310,6 +337,10 @@ Motivo:. %1$s" "Sicurezza" "Visto da" "Seleziona un account" + + "%1$d selezionato" + "%1$d selezionati" + "Invia a" "Invio in corso…" "Invio fallito" @@ -320,12 +351,16 @@ Motivo:. %1$s" "URL del server" "Impostazioni" "Condividi lo spazio" + "I nuovi membri vedono la cronologia" + "Posizione in tempo reale condivisa" "Posizione condivisa" "Spazio condiviso" - "Disconnessione" + "Rimozione del dispositivo" "Qualcosa è andato storto" "Abbiamo riscontrato un problema. Per favore riprova." "Spazio" + "Membri dello spazio" + "Di cosa tratta questo spazio?" "%1$d Spazio" "%1$d Spazi" @@ -333,18 +368,20 @@ Motivo:. %1$s" "Avvio della conversazione…" "Adesivo" "Operazione riuscita" + "Suggeriti" "Suggerimenti" "Sincronizzazione" "Sistema" "Testo" "Comunicazioni di terze parti" "Discussione" + "Discussioni" "Argomento" - "Di cosa parla questa stanza?" + "Di cosa riguarda questa stanza?" "Impossibile decrittografare" "Inviato da un dispositivo non sicuro" "Non hai accesso a questo messaggio" - "L\'identità verificata del mittente è stata reimpostata" + "L\'identità digitale verificata del mittente è stata reimpostata" "Non è stato possibile spedire inviti a uno o più utenti." "Impossibile inviare inviti" "Sblocca" @@ -369,13 +406,19 @@ Motivo:. %1$s" "Messaggio vocale" "In attesa…" "In attesa del messaggio" + "In attesa della posizione in tempo reale…" + "Chiunque può vedere la cronologia" "Tu" - "L\'identità di %1$s è stata reimpostata. %2$s" - "L\'identità %2$s di %1$s sembra essere cambiata. %3$s" + "%1$s (%2$s) ha condiviso questo messaggio poiché non eri nella stanza quando è stato inviato." + "%1$s ha condiviso questo messaggio poiché non eri nella stanza quando è stato inviato." + "Questa stanza è stata configurata in modo che i nuovi membri possano leggere la cronologia. %1$s" + "%1$sL\'identità digitale di %2$s" + "%1$sL\'identità digitale di %2$s è stata reimpostata. %3$s" "(%1$s)" - "L\'identità di %1$s è stata reimpostata." - "L\'identità %2$s di %1$s è stata reimpostata. %3$s" + "L\'identità digitale di %1$s è stata reimpostata." + "L\'identità digitale %2$s di %1$s è stata reimpostata. %3$s" "Ritira verifica" + "Consenti l\'accesso" "Il link %1$s ti porta ad un altro sito %2$s Sei sicuro di voler continuare?" @@ -405,6 +448,7 @@ Sei sicuro di voler continuare?" "%1$s non è riuscito ad accedere alla tua posizione. Riprova più tardi." "Invio del messaggio vocale fallito." "La stanza non esiste più o l\'invito non è più valido." + "Attiva il GPS per accedere alle funzioni basate sulla posizione." "Messaggio non trovato" "%1$s non ha l\'autorizzazione di accedere alla tua posizione. Puoi attivare l\'accesso nelle impostazioni." "%1$s non ha l\'autorizzazione per accedere alla tua posizione. Attiva l\'accesso di seguito." @@ -424,6 +468,14 @@ Sei sicuro di voler continuare?" "Rimuovi %1$s" "Impostazioni" "Selezione del file multimediale fallita, riprova." + "Apri Element Classic" + "Apri Element Classic sul tuo dispositivo" + "Vai su Impostazioni > Sicurezza & privacy" + "Nella gestione delle chiavi crittografiche, seleziona Recupero dei messaggi cifrati" + "Segui le istruzioni per abilitare l\'archiviazione delle chiavi" + "Torna a %1$s" + "Abilita l\'archivio delle chiavi prima di procedere con %1$s" + "Bentornato" "Premi su un messaggio e scegli “%1$s” per includerlo qui." "Fissa i messaggi importanti così che possano essere trovati facilmente" @@ -431,11 +483,11 @@ Sei sicuro di voler continuare?" "%1$d Messaggi fissati" "Messaggi fissati" - "Stai per accedere al tuo account di %1$s per ripristinare la tua identità. Dopodiché verrai riportato all\'app." - "Non riesci a confermare? Vai al tuo account per ripristinare la tua identità." + "Stai per accedere al tuo account %1$s per reimpostare la tua identità digitale. Al termine, verrai reindirizzato all\'app." + "Non riesci a confermare? Vai al tuo account per reimpostare la tua identità digitale." "Ritira la verifica e invia" "Puoi ritirare la tua verifica e inviare comunque questo messaggio, oppure annullarlo per ora e riprovare più tardi dopo aver riverificato %1$s." - "Il tuo messaggio non è stato inviato perché l\'identità verificata di %1$s è stata reimpostata." + "Il tuo messaggio non è stato inviato perché l\'identità digitale verificata di %1$sè stata reimpostata" "Invia comunque il messaggio" "%1$s sta usando uno o più dispositivi non verificati. Puoi inviare il messaggio in ogni caso, oppure annullarlo e riprovare più tardi quando %2$s avrà verificato tutti i suoi dispositivi." "Il tuo messaggio non è stato inviato perché %1$s non ha verificato tutti i dispositivi." @@ -458,12 +510,16 @@ Sei sicuro di voler continuare?" "Apri in Apple Maps" "Apri in Google Maps" "Apri in OpenStreetMap" - "Condividi questa posizione" + "Condividi la posizione selezionata" + "Opzioni di condivisione" "Spazi che hai creato o a cui hai aderito." "%1$s • %2$s" + "Crea spazi per organizzare le stanze" "%1$s spazio" "Spazi" - "Messaggio non inviato perché l\'identità verificata di %1$s è stata reimpostata." + "Condivisa %1$s" + "Sulla mappa" + "Il messaggio non è stato inviato perché l\'identità digitale verificata di %1$sè stata reimpostata." "Messaggio non inviato perché %1$s non ha verificato tutti i dispositivi." "Messaggio non inviato perché non hai verificato uno o più dispositivi." "Posizione" @@ -473,5 +529,5 @@ Sei sicuro di voler continuare?" "È necessario verificare questo dispositivo per accedere alla cronologia messaggi" "Non hai accesso a questo messaggio" "Impossibile decifrare il messaggio" - "Questo messaggio è stato bloccato perché il dispositivo non è verificato o perché il mittente deve verificare la tua identità." + "Questo messaggio è stato bloccato perché non hai verificato il tuo dispositivo o perché il mittente deve verificare la tua identità digitale." diff --git a/libraries/ui-strings/src/main/res/values-ja/translations.xml b/libraries/ui-strings/src/main/res/values-ja/translations.xml new file mode 100644 index 0000000000..f77a92966b --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ja/translations.xml @@ -0,0 +1,525 @@ + + + "リアクションを追加: %1$s" + "アドレス" + "アバター" + "入力欄を縮小" + "削除" + + "%1$d 桁入力済" + + "アバターを編集" + "完全なアドレスは %1$s になります。" + "暗号化の詳細" + "入力欄を拡大" + "パスワードを非表示" + "通話に参加" + "一番下へ" + "現在地に移動" + "メンションのみ" + "ミュート有効" + "新規メンション" + "新着メッセージ" + "通話中" + "他のユーザーのアバター" + "%1$d ページ" + "一時停止" + "音声メッセージ 長さ: %1$s 再生位置: %2$s" + "PIN入力欄" + "ピン留めした位置情報" + "再生" + "再生速度" + "投票" + "投票終了" + "QRコード" + "リアクション: %1$s" + "他の絵文字でリアクション" + "既読: %1$s, %2$s" + + "既読: %1$s ほか %2$d 人" + + "%1$s 既読" + "タップしてすべて表示" + "%1$s リアクションを削除" + "%1$s リアクションを削除" + "ルームのアバター" + "ファイルを送信" + "送信者の位置情報" + "1分以内に検証を完了してください" + "パスワードを表示" + "通話を開始" + "ビデオ通話を開始" + "音声通話を開始" + "埋没したルーム" + "ユーザーのアバター" + "ユーザーメニュー" + "アバターを表示" + "詳細を表示" + "音声メッセージ 長さ: %1$s" + "音声メッセージを録音してください。" + "録音を停止" + "あなたのアバター" + "承諾" + "キャプションを追加" + "既存のルームを追加" + "タイムラインに追加" + "戻る" + "通話" + "キャンセル" + "今回のみキャンセル" + "写真を選択" + "クリア" + "閉じる" + "検証してください" + "確認" + "パスワードを確認" + "続行" + "コピー" + "キャプションをコピー" + "リンクをコピー" + "リンクをメッセージにコピー" + "テキストをコピー" + "作成" + "ルームを作成" + "スペースを作成" + "停止" + "アカウントを無効化" + "拒否" + "拒否してブロック" + "投票を削除" + "全ての選択を解除" + "無効化" + "破棄" + "無視" + "完了" + "編集" + "キャプションを編集" + "投票を編集" + "有効化" + "投票を終了" + "PINを入力" + "公開スペースを探す" + "完了" + "パスワードをお忘れですか?" + "転送" + "戻る" + "役割と権限に移動" + "設定に移動" + "無視" + "招待" + "ユーザーを招待" + "%1$s にユーザーを招待" + "%1$s にユーザーを招待" + "招待" + "参加" + "詳細" + "退出" + "会話を退出" + "ルームを退出" + "スペースを退出" + "さらに表示" + "アカウントを管理" + "アカウントと端末を管理" + "端末を管理" + "ルームを管理" + "メッセージ" + "最小化" + "次へ" + "いいえ" + "後で" + "OK" + "コンテキストメニューを開く" + "設定" + "他のアプリで開く" + "ピン留め" + "クイック返信" + "引用" + "リアクション" + "拒否" + "削除" + "キャプションを削除" + "メッセージを削除" + "返信" + "スレッドで返信" + "報告" + "バグを報告" + "コンテンツを報告" + "会話を通報" + "ルームを通報" + "リセット" + "IDをリセット" + "再試行" + "復号化を再試行" + "保存" + "検索" + "すべて選択" + "送信" + "編集したメッセージを送信" + "メッセージを送信" + "音声メッセージを送信" + "共有" + "リンクを共有" + "ライブ位置情報を共有" + "表示" + "再度サインイン" + "この端末を削除" + "強制的に削除" + "スキップ" + "開始" + "チャットを開始" + "やり直す" + "検証を開始" + "タップして地図を読み込む" + "停止" + "写真を撮影" + "タップしてオプションを表示" + "翻訳" + "もう一度やり直してください" + "ピン留めを解除" + "表示" + "タイムラインで表示" + "ソースコードを表示" + "はい" + "再試行する" + "使用しているサーバーがより高速な新しいプロトコルに対応しました。将来、古いプロトコルへの対応が打ち切られ、強制的にログアウトされる可能性があるため、今すぐにログアウトし、再度ログインし直すことを推奨します。" + "アップグレードがあります" + "アプリケーションについて" + "利用規定" + "アカウントを追加" + "別のアカウントを追加" + "キャプションを追加" + "高度な設定" + "画像" + "分析" + "通知を同期中…" + "あなたがルームを退出" + "セッションからログアウトされました" + "外観" + "音声" + "ベータ版" + "ブロックしたユーザー" + "ふきだし" + "通話を開始しました" + "チャットをバックアップ" + "クリップボードにコピーしました" + "著作権" + "ルームを作成中…" + "スペースを作成中…" + "リクエストはキャンセルされました" + "ルームを退出しました" + "スペースから退出しました" + "招待は却下されました" + "ダーク" + "復号化エラー" + "詳細" + "開発者向けオプション" + "端末ID" + "ダイレクトチャット" + "次回からは表示しない" + "ダウンロードに失敗しました" + "ダウンロード中" + "(編集済み)" + "編集中" + "キャプションを編集" + "* %1$s %2$s" + "空のファイル" + "暗号化" + "暗号化が有効です" + "%1$s に終了" + "PINを入力してください" + "エラー" + "問題が発生しました。新着のメッセージの通知を受け取れない可能性があります。設定から通知のトラブルシューティングを行ってください。 + +理由: %1$s" + "全員" + "失敗" + "お気に入り" + "お気に入り" + "ファイル" + "ファイルを削除しました" + "ファイルを保存しました" + "ファイルはダウンロードに保存されました" + "メッセージを転送" + "頻繁に使用" + "GIF" + "画像" + "%1$s に返信" + "APK をインストール" + "このMatrix IDは見つからないため、招待が届かない可能性があります。" + "ルームを退出しています" + "スペースを退出しています" + "ライト" + "行をクリップボードにコピーしました" + "リンクをクリップボードにコピーしました" + "新しい端末から接続" + "ライブ位置情報" + "ライブ位置情報が終了しました" + "読み込み中…" + "読み込み中…" + + "他 %d 人" + + + "%1$d 人のメンバー" + + "メッセージ" + "メッセージアクション" + "メッセージの送信に失敗" + "メッセージのレイアウト" + "メッセージは削除されました" + "モダン" + "ミュート" + "名前" + "%1$s (%2$s)" + "結果なし" + "ルーム名なし" + "スペース名なし" + "暗号化されていません" + "オフライン" + "オープンソースライセンス" + "または" + "他のオプション" + "パスワード" + "人" + "固定リンク" + "権限" + "ピン留め" + "インターネット接続を確認してください" + "お待ちください…" + "本当に投票を終了しますか?" + "投票: %1$s" + "総投票数:%1$s" + "結果は投票終了後に表示されます" + + "%d 票" + + "準備中…" + "プライバシーポリシー" + "非公開" + "非公開ルーム" + "非公開スペース" + "公開" + "公開ルーム" + "公開スペース" + "リアクション" + "リアクション" + "理由" + "回復鍵" + "更新中…" + "削除しています…" + + "%1$d 件の返信" + + "%1$s に返信" + "バグを報告" + "問題を報告" + "報告は送信されました" + "リッチテキストエディター" + "役割" + "ルーム" + "ルーム名" + "例: プロジェクトの名称" + + "%1$d 個のルーム" + + "保存された変更" + "保存中…" + "画面ロック" + "ユーザーを検索" + "検索結果" + "セキュリティ" + "既読" + "アカウントを選択" + + "%1$d 個を選択" + + "送る:" + "送信中…" + "送信失敗" + "送信済" + "。" + "このサーバーには対応していません" + "サーバーに接続できません" + "サーバー URL" + "設定" + "スペースを共有" + "新しいメンバーは履歴を閲覧できます" + "共有したライブ位置情報" + "共有された位置情報" + "共有されたスペース" + "端末を削除中" + "問題が発生しました" + "問題が発生しました。再度お試しください。" + "スペース" + "スペースのメンバー" + "このスペースは何についてのものですか?" + + "%1$d 個のスペース" + + "チャットを開始しています…" + "ステッカー" + "成功" + "推奨される" + "提案" + "同期中" + "システム" + "テキスト" + "第三者に関する通知" + "スレッド" + "スレッド" + "トピック" + "何についてのルームですか?" + "復号化できません" + "安全でないデバイスから送信されました" + "このメッセージにアクセスできません" + "送信者の検証済みのデジタルIDがリセットされました" + "1人以上のユーザーに招待を送信できませんでした。" + "招待を送信できません" + "ロック解除" + "ミュート解除" + "非対応の通話" + "非対応のイベント" + "ユーザー名" + "検証がキャンセルされました" + "検証完了" + "検証に失敗しました" + "検証済み" + "端末を検証" + "IDを検証" + "ユーザーの検証" + "動画" + "高画質" + "高画質・ファイルサイズ大" + "低画質" + "最速アップロード・ファイルサイズ小" + "標準画質" + "中程度の画質とアップロード速度" + "音声メッセージ" + "待機中…" + "メッセージを待機中" + "ライブ位置情報を待機中…" + "誰でも履歴を閲覧できます" + "あなた" + "あなたが参加する以前に送信されたメッセージを、%1$s (%2$s) が共有しました。" + "あなたが参加する以前に送信されたメッセージを、%1$s が共有しました。" + "このルームは新しいメンバーが過去の内容を確認できるように設定されています。%1$s" + "%1$s のデジタルIDはリセットされました。%2$s" + "%1$s の %2$s のデジタルIDはリセットされました。%3$s" + "(%1$s)" + "%1$s のデジタルIDはリセットされました。" + "%1$s %2$s のデジタルIDはリセットされました。%3$s" + "検証のリクエストを却下" + "アクセスを許可" + "リンク %1$s が別サイト %2$s に遷移しようとしています。 + +続行しますか?" + "リンクを再度確認してください" + "アップロードする動画のデフォルト画質を選択してください。" + "動画のアップロード品質" + "アップロードできる最大サイズは %1$s です。" + "アップロードの許容サイズを超えています。" + "ルームを通報しました" + "ルームを通報し退出" + "確認" + "エラー" + "成功" + "警告" + "未保存の変更内容があります。" + "変更は保存されていません。本当に戻りますか?" + "変更を保存しますか?" + "アップロードできる最大サイズは %1$s です。" + "アップロードする画質を選択してください。" + "動画のアップロード品質を選択" + "絵文字を検索" + "この端末では既に %1$s としてログインしています。" + "Matrix Authentication Service とアカウント作成に対応するため、このホームサーバーはアップグレードが必要です。" + "固定リンクの作成に失敗しました" + "%1$s でマップを読み込めません。時間を置いて再度お試しください。" + "メッセージの読み込みに失敗しました" + "%1$s は現在地にアクセスできませんでした。時間を置いて再度お試しください。" + "音声メッセージの送信に失敗しました。" + "このルームは存在しないか、招待が無効です。" + "位置情報に基づく機能を使用するには、GPSを有効化してください。" + "メッセージが見つかりません" + "現在地を取得する権限が %1$s にありません。設定で権限を追加できます。" + "現在地を取得する権限が %1$s にありません。以下から許可してください。" + "%1$s には、マイクの使用が許可されていません。音声メッセージのために、マイクの使用を許可してください。" + "ネットワークまたはサーバーの問題である可能性があります。" + "このルームアドレスは既に存在しています。ルームの名称を変更するか、アドレスを編集してください。" + "一部の文字は使用できません。使用できるのは、英字、数字と記号 ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _ です。" + "送信されていないメッセージがあります。" + "申し訳ありません。エラーが発生しました。" + "🔐️ %1$s に参加してください" + "%1$s で話しましょう: %2$s" + "%1$s Android" + "開発者にバグを報告するには端末を振ってください。" + "スクリーンショット" + "%1$s: %2$s" + "選択肢" + "%1$s を削除" + "設定" + "ファイルの選択に失敗しました。再試行してください。" + "Element Classic を開く" + "Element Classic をこの端末で開く" + "「設定- セキュリティとプライバシー」に移動します" + "暗号鍵の管理から、暗号化されたメッセージの回復を選択します" + "指示に従って、鍵の保管庫を有効化してください" + "%1$s に戻ってください" + "%1$s に続行する前に、鍵の保管庫を有効化してください" + "アカウントを確認中" + "おかえりなさい" + "メッセージを長押しし \"%1$s\" を選択してください" + "重要なメッセージをピン留めして容易に見つけられるようにします" + + "%1$d 個のピン留めされたメッセージ" + + "ピン留めされたメッセージ" + "デジタルIDをリセットするため %1$s のアカウントの設定に移動します。完了後、アプリに遷移します。" + "認証できませんか?アカウントに移動してデジタルIDをリセットできます。" + "検証の要求を取り下げて送信" + "検証の要求を取り下げてメッセージの送信を続行するか、送信をキャンセルして %1$s の検証が完了した後に再試行することができます。" + "%1$s の検証済みのデジタルIDがリセットされたため、メッセージは送信されませんでした。" + "メッセージを強制送信" + "%1$s は未検証の端末を使用しています。メッセージを強制的に送信するか、%2$s がすべての端末を検証した後に送信を再試行することができます。" + "%1$s に未検証の端末が存在するため、メッセージは送信されませんでした。" + "検証の完了していない端末があります。メッセージを強制的に送信するか、すべての端末を検証した後に送信を再試行することができます。" + "未検証の端末が存在するため、メッセージは送信されませんでした。" + "管理者または所有者を編集" + "ファイルの処理に失敗しました。再試行してください。" + "ユーザーの詳細を取得できませんでした" + "%1$s のメッセージ" + "展開" + "縮小" + "ライブ位置情報を共有中" + "すでにこのルームを表示しています" + "%2$s 個のうち %1$s" + "%1$s 個のピン留めされたメッセージ" + "メッセージを読み込み中…" + "すべて表示" + "チャット" + "場所を共有" + "現在地を共有する" + "Apple Maps で開く" + "Google Maps で開く" + "OpenStreetMapで開く" + "この位置情報を共有する" + "共有設定" + "作成または参加したスペースです。" + "%1$s・%2$s" + "スペースを作成してルームを整頓しましょう" + "%1$s スペース" + "スペース" + "%1$s 共有" + "マップ上" + "%1$s の検証済みのデジタルIDがリセットされたため、メッセージは送信されませんでした。" + "%1$s に未検証の端末が存在するため、メッセージは送信されませんでした。" + "未検証の端末が存在するため、メッセージは送信されませんでした。" + "位置情報" + "バージョン: %1$s (%2$s)" + "ja" + "この端末で過去のメッセージを表示できません" + "過去のメッセージを表示するには、この端末を検証する必要があります" + "このメッセージにアクセスできません" + "メッセージの復号化に失敗" + "この端末を検証していないか、送信者があなたのデジタルIDを検証していないため、このメッセージはブロックされました。" + diff --git a/libraries/ui-strings/src/main/res/values-ko/translations.xml b/libraries/ui-strings/src/main/res/values-ko/translations.xml index a9d560b852..4174e4d15d 100644 --- a/libraries/ui-strings/src/main/res/values-ko/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -26,6 +26,7 @@ "일시중지" "음성 메시지, 지속 시간: %1$s, 현재 위치: %2$s" "PIN 필드" + "고정된 위치" "재생" "재생 속도" "투표" @@ -43,9 +44,11 @@ "%1$s 반응을 제거하세요" "방 아바타" "파일 보내기" + "발신자 위치" "제한 시간 내 인증이 필요합니다.1분 안에 확인해 주세요." "비밀번호 표시" "통화 시작" + "음성 통화 시작" "묘비 방" "사용자 아바타" "사용자 메뉴" @@ -115,6 +118,7 @@ "스페이스 떠나기" "더 불러오기" "계정 관리" + "계정 및 기기 관리" "기기 관리" "방 관리" "메시지" @@ -165,6 +169,7 @@ "다시 시작하다" "인증 시작" "탭해서 지도 불러오기" + "중지" "사진 찍기" "옵션을 보려면 탭하세요" "번역" @@ -185,6 +190,7 @@ "고급 설정" "이미지" "통계" + "알림 동기화 중…" "방을 떠남" "세션에서 로그아웃되었습니다." "외관" @@ -218,6 +224,7 @@ "빈 파일" "암호화" "암호화 활성화됨" + "%1$s에 종료" "PIN을 입력하세요" "오류" "오류가 발생했습니다, 새 메시지 알림을 받지 못할 수 있습니다. 설정에서 알림 문제를 해결하세요. @@ -244,6 +251,8 @@ "줄이 클립보드에 복사되었습니다." "링크가 클립보드에 복사됨" "새 기기 연결" + "실시간 위치" + "실시간 위치 공유 종료" "로딩 중…" "더 많은 내용이 로딩 중…" @@ -268,6 +277,7 @@ "오프라인" "오픈 소스 라이선스" "또는" + "기타 옵션" "비밀번호" "사람" "퍼머링크" @@ -333,6 +343,7 @@ "설정" "스페이스 공유" "새 멤버에게 대화 기록 공개" + "공유 중인 실시간 위치" "공유된 위치" "공유된 스페이스" "기기 제거" @@ -354,12 +365,13 @@ "글자" "제3자 고지" "스레드" + "스레드" "주제" "이 방은 어떤 곳인가요?" "해독 불가" "보안되지 않은 장치에서 전송됨" "이 메시지에 액세스할 수 없습니다" - "발신자의 검증된 신원이 재설정되었습니다." + "발신자의 인증된 디지털 신원이 재설정되었습니다" "한 명 이상의 사용자에게 초대를 보낼 수 없습니다." "초대를 보낼 수 없음" "잠금 해제" @@ -384,16 +396,17 @@ "음성 메시지" "대기 중…" "이 메시지를 기다리고 있습니다" + "실시간 위치 정보를 기다리는 중…" "누구나 대화 기록 보기 가능" "당신" "%1$s(%2$s)님이 이 메시지를 공유했습니다. 메시지 전송 당시 귀하가 방에 참여 중이 아니었기 때문입니다." "%1$s님이 이 메시지를 공유했습니다. 메시지 전송 당시 귀하가 방에 참여 중이 아니었기 때문입니다." "이 방은 새 멤버가 이전 대화 기록을 읽을 수 있도록 설정되었습니다. %1$s" - "%1$s 의 신원이 재설정되었습니다. %2$s" + "%1$s님의 디지털 신원이 재설정되었습니다. %2$s" "%1$s의 %2$s 신원이 재설정되었습니다. %3$s" "(%1$s)" - "%1$s의 신원이 재설정되었습니다." - "%1$s의 %2$s 신원이 재설정되었습니다. %3$s" + "%1$s의 디지털 신원이 재설정되었습니다." + "%1$s님의 %2$s 디지털 신원이 재설정되었습니다. %3$s" "확인 취소" "액세스 허용" "%1$s 링크는 다른 사이트로 이동합니다 %2$s @@ -427,6 +440,7 @@ "%1$s가 위치에 접근할 수 없습니다. 나중에 다시 시도해 주세요." "음성 메시지 업로드에 실패했습니다." "해당 방이 더 이상 존재하지 않거나 초대장이 더 이상 유효하지 않습니다." + "위치 기반 기능을 사용하려면 GPS를 켜 주세요." "메시지를 찾을 수 없습니다" "%1$s에서 위치에 접근할 수 있는 권한이 없습니다. 설정에서 활성화가 가능합니다." "%1$s에서 위치에 접근할 수 있는 권한이 없습니다. 아래에서 허용해주세요." @@ -446,6 +460,14 @@ "%1$s 제거" "설정" "미디어 선택에 실패했습니다. 다시 시도해 주세요." + "Element Classic 열기" + "기기에서 Element Classic 앱을 열어 주세요" + "설정 > 보안 및 개인정보 보호로 이동하세요" + "암호화 키 관리에서 \'암호화된 메시지 복구\'를 선택하세요" + "안내에 따라 키 저장소를 활성화해 주세요" + "%1$s(으)로 돌아가기" + "%1$s(으)로 진행하기 전에 키 저장소를 활성화해 주세요." + "다시 오신 것을 환영합니다" "메시지를 누르고 \"%1$s\" 를 선택하여 여기에 포함합니다." "중요한 메시지를 고정하여 쉽게 찾을 수 있도록 합니다" @@ -456,7 +478,7 @@ "확인할 수 없나요? 계정 설정으로 이동하여 디지털 신원을 재설정하세요." "인증 철회 및 전송" "확인 절차를 철회하고 이 메시지를 보내거나, 지금 취소하고 나중에 %1$s 을 확인한 후 다시 시도할 수 있습니다." - "%1$s의 인증된 신원이 재설정되어 귀하의 메시지가 전송되지 않았습니다." + "%1$s님의 인증된 디지털 신원이 재설정되어 메시지를 전송하지 못했습니다." "아무튼 메시지 보내기" "%1$s 는 하나 이상의 확인되지 않은 장치를 사용하고 있습니다. 메시지를 보내거나, %2$s 이 모든 장치를 확인한 후에 다시 시도할 수 있습니다." "%1$s 이(가) 모든 기기를 확인하지 않았기 때문에 귀하의 메시지가 전송되지 않았습니다." @@ -480,12 +502,15 @@ "Google Maps에서 열기" "OpenStreetMap에서 열기" "선택한 위치 공유" + "공유 옵션" "당신이 스페이스를 만들거나 가입했습니다." "%1$s•%2$s" "스페이스를 생성하여 방을 체계적으로 관리해 보세요." "%1$s 스페이스" "스페이스" - "%1$s의 인증된 신원이 재설정되어 메시지가 전송되지 않았습니다." + "공유된 %1$s" + "지도에서 보기" + "%1$s님의 인증된 디지털 신원이 재설정되어 메시지를 보내지 못했습니다." "%1$s 이 모든 장치를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다." "하나 이상의 기기를 확인하지 않았기 때문에 메시지가 전송되지 않았습니다." "위치" @@ -495,5 +520,5 @@ "이전 메시지에 액세스하려면 이 장치를 확인해야 합니다." "이 메시지에 액세스할 수 없습니다" "메시지를 해독할 수 없습니다." - "이 메시지는 귀하가 기기를 확인하지 않았거나 발신자가 귀하의 신원을 확인해야 하기 때문에 차단되었습니다." + "기기가 인증되지 않았거나, 발신자가 귀하의 디지털 신원을 확인해야 하므로 이 메시지가 차단되었습니다." diff --git a/libraries/ui-strings/src/main/res/values-lt/translations.xml b/libraries/ui-strings/src/main/res/values-lt/translations.xml index 18eafdb13c..1ebbceedd5 100644 --- a/libraries/ui-strings/src/main/res/values-lt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-lt/translations.xml @@ -88,7 +88,7 @@ "Kūrėjo nustatymai" "Asmeninis pokalbis" "(redaguota)" - "Taisymas" + "Redagavimas" "* %1$s %2$s" "Šifravimas įjungtas" "Klaida" @@ -96,7 +96,7 @@ "Failas išsaugotas aplanke Atsisiuntimai" "Persiųsti žinutę" "GIF" - "Paveikslėlis" + "Vaizdas" "Šio Matrix ID nepavyksta rasti, todėl kvietimas gali būti negautas." "Paliekamas kambarys" "Nuoroda nukopijuota į iškarpinę" @@ -170,6 +170,7 @@ "%1$s Android" "Papurtykite, kad praneštumėte apie klaidą" "Nepavyko pasirinkti laikmenos, pabandykite dar kartą." + "Sveiki sugrįžę" "Nepavyko apdoroti įkeliamos laikmenos, bandykite dar kartą." "Nepavyko gauti naudotojo išsamios informacijos." "Bendrinti vietą" diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml index bd6eaab990..6812885c9c 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -1,6 +1,7 @@ "Dodaj reakcję: %1$s" + "Adres" "Awatar" "Zmniejsz pole tekstowe wiadomości" "Usuń" @@ -27,6 +28,7 @@ "Wstrzymaj" "Wiadomość głosowa, czas trwania: %1$s, aktualna pozycja: %2$s" "Pole PIN" + "Przypięta lokalizacja" "Odtwórz" "Ankieta" "Zakończona ankieta" @@ -148,8 +150,8 @@ "Wyślij edytowaną wiadomość" "Wyślij wiadomość" "Wyślij wiadomość głosową" - "Udostępnij" - "Udostępnij link" + "Wyślij" + "Wyślij link" "Pokaż" "Zaloguj się ponownie" "Wyloguj" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 4ae92c03ea..5e4d7f9b67 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -52,6 +52,7 @@ "Требуется действие, на которое есть ограничение по времени, у вас есть одна минута для проверки" "Показать пароль" "Начать звонок" + "Начать видеозвонок" "Начать голосовой вызов" "Брошенная комната" "Аватар пользователя" @@ -122,6 +123,7 @@ "Покинуть пространство" "Загрузить еще" "Настройки аккаунта" + "Управление учетной записью и устройствами" "Управление устройствами" "Управление комнатами" "Написать" @@ -142,7 +144,7 @@ "Удалить подпись" "Удалить сообщение" "Ответить" - "Ответить в ветке" + "Обсудить" "Пожаловаться" "Сообщить об ошибке" "Пожаловаться на содержание" @@ -172,6 +174,7 @@ "Начать заново" "Начать подтверждение" "Нажмите, чтобы загрузить карту" + "Остановить" "Сделать фото" "Нажмите для просмотра вариантов" "Перевести" @@ -226,6 +229,7 @@ "Пустой файл" "Шифрование" "Шифрование включено" + "Время завершения: %1$s" "Введите свой PIN-код" "Ошибка" "Произошла ошибка. Вы можете не получать уведомления о новых сообщениях. Устраните неполадки с уведомлениями в настройках. @@ -379,7 +383,8 @@ "Системное" "Текст" "Уведомления о третьих лицах" - "Ветка" + "Обсуждение" + "Обсуждения" "Тема" "О чем эта комната?" "Невозможно расшифровать" @@ -472,6 +477,13 @@ "Удалить %1$s" "Настройки" "Не удалось выбрать медиа, попробуйте еще раз." + "Открыть Element Classic" + "Откройте Element Classic на своем устройстве." + "Перейдите в Настройки > Безопасность и конфиденциальность" + "В разделе «Управление криптографическими ключами» выбери «Восстановление зашифрованных сообщений»" + "Следуйте инструкциям, чтобы активировать хранилище ключей" + "Вернитесь к %1$s" + "Перед продолжением активируйте хранилище ключей %1$s" "С возвращением" "Нажмите на сообщение и выберите «%1$s», чтобы добавить его сюда." "Закрепите важные сообщения, чтобы их можно было легко найти" @@ -485,7 +497,7 @@ "Не можете подтвердить? Перейдите в свой аккаунт, чтобы сбросить свою личность." "Сбросить верификацию и отправить" "Вы можете либо сбросить подтверждение и всё равно отправить это сообщение, либо отменить его сейчас и повторить попытку после повторного подтверждения %1$s." - "Ваше сообщение не было отправлено, потому что подтвержденная личность %1$s была сброшена" + "Ваше сообщение не было отправлено, потому что подтвержденная идентификации %1$s была сброшена" "Все равно отправить сообщение" "У %1$s есть одно или несколько неподтвержденных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже, когда %2$s подтвердить все свои устройства." "Ваше сообщение не было отправлено, потому что %1$s имеет неподтвержденные устройства" @@ -497,6 +509,7 @@ "Сообщение в %1$s" "Развернуть" "Уменьшить" + "Поделиться текущим местоположением…" "Эта комната уже просматривается!" "%1$s из %2$s" "%1$s закрепленные сообщения" @@ -517,7 +530,7 @@ "Пространства" "Поделился %1$s" "На карте" - "Сообщение не отправлено, потому что подтвержденная личность %1$s была сброшена." + "Сообщение не отправлено, так как подтвержденная цифровая идентичность %1$s была сброшена." "Сообщение не отправлено, потому что %1$s не подтвердил(а) все свои устройства." "Сообщение не отправлено, поскольку вы не подтвердили одно или несколько своих устройств." "Местоположение" @@ -527,5 +540,5 @@ "Вам необходимо подтвердить это устройство чтобы получить доступ к истории сообщений" "Вы не имеете доступа к этому сообщению" "Не удалось расшифровать сообщение" - "Это сообщение было заблокировано, так как вы не подтвердили свое устройство, либо отправителю необходимо подтвердить вашу личность." + "Это сообщение было заблокировано либо потому, что вы не подтвердили свое устройство, либо потому, что отправителю необходимо подтвердить вашу цифровую личность." diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml index 0969270a3c..dfcd514209 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -146,8 +146,8 @@ "Dela länk" "Visa" "Logga in igen" - "Logga ut" - "Logga ut ändå" + "Ta bort den här enheten" + "Ta bort den här enheten ändå" "Hoppa över" "Starta" "Starta chat" diff --git a/libraries/ui-strings/src/main/res/values-vi/translations.xml b/libraries/ui-strings/src/main/res/values-vi/translations.xml new file mode 100644 index 0000000000..1cc07bb8cf --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-vi/translations.xml @@ -0,0 +1,425 @@ + + + "Thêm biểu cảm: %1$s" + "Địa chỉ" + "Ảnh đại diện" + "Thu nhỏ ô nhập tin nhắn" + "Xóa" + + "Đã nhập %1$d chữ số" + + "Đổi ảnh đại diện" + "Đường dẫn đầy đủ của phòng là %1$s" + "Chi tiết mã hóa" + "Mở rộng ô nhập tin nhắn" + "Ẩn mật khẩu" + "Tham gia cuộc gọi" + "Xuống cuối" + "Di chuyển bản đồ đến vị trí của tôi" + "Chỉ khi được đề cập tới" + "Tắt thông báo" + "Đề cập mới" + "Tin nhắn mới" + "Cuộc gọi hiện thời" + "Ảnh đại diện của người dùng khác" + "Trang %1$d" + "Tạm dừng" + "Tin nhắn thoại, thời lượng: %1$s, vị trí hiện tại: %2$s" + "Trường mã PIN" + "Vị trí được ghim" + "Phát" + "Tốc độ phát" + "Bỏ phiếu" + "Đã kết thúc cuộc thăm dò" + "Mã QR" + "Phản ứng với %1$s" + "Phản ứng với các biểu tượng cảm xúc khác" + "Đọc bởi %1$s và %2$s" + + "%1$s và %2$d người khác đã đọc." + + "Đọc bởi %1$s" + "Nhấn để hiển thị tất cả" + "Xóa phản ứng: %1$s" + "Loại bỏ phản ứng với %1$s" + "Ảnh đại diện phòng" + "Gửi tệp" + "Vị trí người gửi" + "Yêu cầu hành động có giới hạn thời gian, bạn có một phút để xác minh" + "Hiện mật khẩu" + "Gọi" + "Bắt đầu cuộc gọi thoại" + "Phòng Tombstone" + "Ảnh đại diện của người dùng" + "Menu người dùng" + "Xem ảnh đại diện" + "Xem chi tiết" + "Tin nhắn thoại, thời lượng: %1$s" + "Ghi âm tin nhắn thoại" + "Dừng ghi" + "Ảnh đại diện của bạn" + "Đồng ý" + "Thêm chú thích" + "Thêm các phòng trò chuyện hiện có" + "Thêm vào dòng thời gian" + "Quay lại" + "Gọi" + "Hủy" + "Hủy ngay" + "Chọn ảnh" + "Xoá" + "Đóng" + "Xác minh hoàn tất" + "Xác nhận" + "Xác nhận mật khẩu" + "Tiếp tục" + "Chép" + "Sao chép chú thích" + "Chép liên kết" + "Sao chép liên kết đến tin nhắn" + "Chép văn bản" + "Tạo" + "Tạo phòng" + "Tạo không gian" + "Hủy kích hoạt" + "Vô hiệu hóa tài khoản" + "Từ chối" + "Từ chối và chặn" + "Xóa cuộc thăm dò" + "Bỏ chọn tất cả" + "Tắt" + "Hủy" + "Bỏ qua" + "Xong" + "Chỉnh sửa" + "Chỉnh sửa chú thích" + "Sửa cuộc thăm dò" + "Bật" + "Kết thúc cuộc thăm dò" + "Nhập mã PIN" + "Khám phá các không gian công cộng" + "Hoàn tất" + "Quên mật khẩu?" + "Chuyển tiếp" + "Quay lại" + "Đi tới vai trò và quyền" + "Vào cài đặt" + "Bỏ qua" + "Mời" + "Mời ai đó" + "Mời ai đó vào %1$s" + "Mời ai đó vào %1$s" + "Lời mời" + "Tham gia" + "Tìm hiểu thêm" + "Rời" + "Rời khỏi cuộc trò chuyện" + "Rời phòng" + "Rời space" + "Tải thêm" + "Quản lý tài khoản" + "Quản lý tài khoản và thiết bị" + "Quản lý thiết bị" + "Quản lý phòng trò chuyện" + "Tin nhắn" + "Thu nhỏ" + "Tiếp" + "Không" + "Không phải lúc này" + "OK" + "Mở context menu" + "Cài đặt" + "Mở bằng" + "Ghim" + "Trả lời nhanh" + "Trích dẫn" + "Phản ứng" + "Từ chối" + "Xoá" + "Xóa chú thích" + "Xóa tin nhắn" + "Trả lời" + "Trả lời trong thread" + "Báo cáo" + "Báo lỗi" + "Báo cáo nội dung" + "Báo cáo cuộc trò chuyện" + "Báo cáo phòng" + "Đặt lại" + "Đặt lại danh tính" + "Thử lại" + "Thử giải mã lại" + "Lưu" + "Tìm kiếm" + "Chọn tất cả" + "Gửi" + "Gửi tin nhắn đã chỉnh sửa" + "Gửi tin nhắn" + "Gửi tin nhắn thoại" + "Chia sẻ" + "Chia sẻ liên kết" + "Chia sẻ vị trí trong thời gian thực" + "Hiện" + "Đăng nhập lại" + "Gỡ bỏ thiết bị này" + "Vẫn gỡ bỏ thiết bị này" + "Bỏ qua" + "Bắt đầu" + "Bắt đầu trò truyện" + "Bắt đầu lại" + "Bắt đầu xác thực" + "Nhấn để tải bản đồ" + "Dừng" + "Chụp ảnh" + "Nhấn để hiện tùy chọn" + "Dịch" + "Thử lại" + "Bỏ ghim" + "Xem" + "Xem trong dòng thời gian" + "Xem mã nguồn" + "Có" + "Có, thử lại" + "Máy chủ của bạn hiện đã hỗ trợ một giao thức mới, nhanh hơn. Hãy đăng xuất và đăng nhập lại để nâng cấp ngay. Việc này sẽ giúp bạn tránh bị buộc đăng xuất khi giao thức cũ bị loại bỏ sau này." + "Có thể nâng cấp" + "Giới thiệu" + "Quy định sử dụng dịch vụ" + "Thêm tài khoản" + "Thêm tài khoản khác" + "Thêm chú thích" + "Cài đặt nâng cao" + "một hình ảnh" + "Phân tích" + "Đang đồng bộ thông báo…" + "Bạn rời phòng" + "Bạn đã bị đăng xuất" + "Giao diện" + "Âm thanh" + "Thử nghiệm" + "Người dùng bị chặn" + "Bong bóng" + "Cuộc gọi bắt đầu" + "Sao lưu cuộc trò chuyện" + "Đã sao chép" + "Bản quyền" + "Đang tạo phòng…" + "Tạo không gian…" + "Yêu cầu bị hủy bỏ" + "Đã rời khỏi phòng" + "Rời khỏi không gian" + "Lời mời bị từ chối" + "Tối" + "Lỗi khi giải mã" + "Mô tả" + "Tùy chọn nhà phát triển" + "ID thiết bị" + "Chat trực tiếp" + "Không hiển thị lại" + "Tải xuống thất bại" + "Đang tải xuống" + "(đã sửa)" + "Đang chỉnh sửa" + "Chỉnh sửa chú thích" + "*%1$s%2$s" + "Tệp trống." + "Mã hóa" + "Đã bật mã hoá" + "Kết thúc lúc %1$s" + "Nhập mã PIN của bạn" + "Lỗi" + "Đã xảy ra lỗi, bạn có thể không nhận được thông báo cho tin nhắn mới. Vui lòng khắc phục sự cố thông báo trong phần cài đặt. + +Lý do: %1$s ." + "Mọi người" + "Thất bại" + "Yêu thích" + "Được yêu thích" + "Tập tin" + "Tệp đã bị xóa" + "Tệp đã được lưu" + "Tệp đã được lưu vào thư mục Tải xuống" + "Chuyển tiếp tin nhắn" + "Thường được sử dụng" + "Ảnh động" + "Ảnh" + "Trả lời %1$s" + "Cài đặt APK" + "Không tìm thấy Matrix ID này, nên lời mời có thể chưa được nhận." + "Rời khỏi phòng" + "Rời khỏi không gian" + "Sáng" + "Đã sao chép dòng" + "Đã chép liên kết vào bộ nhớ tạm" + "Liên kết thiết bị mới" + "Vị trí trong thời gian thực" + "Chia sẻ vị trí trực tiếp đã kết thúc" + "Đang tải" + "Đang tải thêm…" + + "%d người khác" + + + "%1$d số thành viên" + + "Tin nhắn" + "Thao tác tin nhắn" + "Không gửi được tin nhắn" + "Bố cục tin nhắn" + "Tin nhắn bị xoá" + "Hiện đại" + "Tắt tiếng" + "Tên" + "Không có kết quả" + "Không được mã hóa" + "Ngoại tuyến" + "hoặc" + "Mật khẩu" + "Danh bạ" + "Liên kết cố định" + "Quyền truy cập" + "Bạn có chắc chắn muốn kết thúc cuộc thăm dò này không?" + "Khảo sát: %1$s" + "Tổng số phiếu: %1$s" + "Kết quả sẽ hiển thị sau khi cuộc thăm dò kết thúc" + + "%d lượt bình chọn" + + "Chính sách bảo mật" + "Phòng riêng tư" + "Biểu cảm" + "Cảm xúc" + "Khóa khôi phục." + "Đang làm mới…" + "Đang trả lời cho %1$s" + "Báo cáo lỗi" + "Báo cáo sự cố" + "Đã gửi báo cáo" + "Trình soạn thảo văn bản nâng cao" + "Phòng" + "Tên phòng" + "ví dụ: tên dự án của bạn" + "Đã lưu thay đổi" + "Đang lưu" + "Khóa màn hình" + "Tìm kiếm ai đó" + "Kết quả tìm kiếm" + "Bảo mật" + "Được xem bởi" + "Đang gửi…" + "Không gửi được" + "Đã gửi" + "Máy chủ không được hỗ trợ" + "Không thể kết nối với máy chủ" + "URL máy chủ" + "Cài đặt" + "Chia sẻ không gian" + "Thành viên mới có thể xem lịch sử." + "Chia sẻ vị trí trực tiếp" + "Vị trí được chia sẻ" + "Không gian chung" + "Đang gỡ thiết bị" + "Đã xảy ra sự cố" + "Đã xảy ra lỗi. Vui lòng thử lại." + "Không gian" + "Thành viên không gian" + "Không gian này dùng để làm gì?" + "Đang bắt đầu cuộc trò chuyện…" + "Sticker" + "Thành công" + "Gợi ý" + "Gợi ý" + "Đang đồng bộ" + "Hệ thống" + "Văn bản" + "Thông báo từ bên thứ ba" + "Chủ đề" + "Chủ đề" + "Chủ đề" + "Phòng này dùng để làm gì?" + "Không thể giải mã" + "Bạn không thể xem tin nhắn này" + "Không thể gửi lời mời đến một hoặc nhiều người dùng." + "Không thể gửi lời mời" + "Mở khóa" + "Bật tiếng" + "Cuộc gọi không được hỗ trợ" + "Sự kiện không được hỗ trợ" + "Tên người dùng" + "Đã hủy xác thực" + "Xác minh hoàn tất" + "Xác minh thất bại" + "Đã xác minh" + "Xác minh thiết bị" + "Xác minh danh tính" + "Xác minh người dùng" + "Video" + "Chất lượng cao" + "Chất lượng tốt nhất nhưng dung lượng tệp lớn hơn" + "Chất lượng thấp" + "Tốc độ tải lên nhanh nhất và kích thước tệp nhỏ nhất" + "Chất lượng tiêu chuẩn" + "Cân bằng giữa chất lượng và tốc độ tải lên" + "Tin nhắn thoại" + "Đang chờ…" + "Đang chờ tin nhắn này" + "Đang chờ vị trí trực tiếp…" + "Ai cũng có thể xem lịch sử" + "Bạn" + "%1$s (%2$s) đã chia sẻ tin nhắn này vì bạn không có trong phòng khi nó được gửi." + "%1$s đã chia sẻ tin nhắn này vì bạn không có trong phòng khi nó được gửi." + "Phòng chat này đã được thiết lập để các thành viên mới có thể xem lịch sử trò chuyện. %1$s" + "Danh tính số của %1$s đã được đặt lại. %2$s" + "Danh tính số %2$s của %1$s đã được đặt lại. %3$s" + "(%1$s )" + "Danh tính số của %1$s đã được đặt lại." + "Danh tính số %2$s của %1$s đã được đặt lại. %3$s" + "Hủy xác minh" + "Cho phép truy cập" + "Liên kết %1$s sẽ đưa bạn đến một trang khác %2$s + +Bạn có chắc muốn tiếp tục không?" + "Kiểm tra lại liên kết này" + "Chọn chất lượng mặc định cho video bạn tải lên." + "Kích thước tệp tối đa cho phép là: %1$s" + "Kích thước tệp quá lớn để tải lên" + "Phòng đã được báo cáo" + "Xác nhận" + "Lỗi" + "Thành công" + "Cảnh báo" + "Bạn có thay đổi chưa được lưu." + "Các thay đổi của bạn chưa được lưu. Bạn có chắc muốn quay lại không?" + "Lưu thay đổi?" + "Kích thước tệp tối đa cho phép là: %1$s" + "Chọn chất lượng video bạn muốn tải lên." + "Chọn chất lượng tải lên video" + "Không tạo được liên kết cố định" + "%1$s không thể tải bản đồ. Vui lòng thử lại sau." + "Không tải được tin nhắn" + "%1$s không thể truy cập vị trí của bạn. Vui lòng thử lại sau." + "Không thể tải lên tin nhắn thoại của bạn." + "%1$s không có quyền truy cập vị trí của bạn. Bạn có thể bật quyền trong Cài đặt." + "%1$s chưa được phép truy cập vị trí. Bật quyền dưới đây." + "%1$s không có quyền truy cập micro của bạn. Hãy bật quyền để ghi tin nhắn thoại." + "Một số tin nhắn chưa được gửi" + "Rất tiếc, đã có lỗi xảy ra." + "🔐️ Tham gia cùng tôi trên %1$s" + "Xin chào, hãy trò chuyện với tôi trên %1$s bằng đường liên kết sau: %2$s" + "%1$s Android" + "Lắc điện thoại để báo cáo lỗi" + "Không thể chọn tệp phương tiện. Vui lòng thử lại." + "Tin nhắn được ghim" + "Xử lý phương tiện tải lên không thành công, vui lòng thử lại." + "Không thể lấy thông tin người dùng" + "%1$s Tin nhắn được ghim" + "Chia sẻ vị trí" + "Chia sẻ vị trí của tôi" + "Mở trong Apple Maps" + "Mở trong Google Maps" + "Mở trong OpenStreetMap" + "Chia sẻ vị trí đã chọn" + "Vị trí" + "Phiên bản: %1$s (%2$s )" + "en" + "Bạn không thể xem tin nhắn này" + diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml index 44f7c1c1e6..1ce3a75ced 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -9,7 +9,7 @@ "已输入 %1$d 个数字" "编辑头像" - "完整地址为%1$s" + "完整地址为 %1$s" "加密详情" "展开消息文本框" "隐藏密码" @@ -26,11 +26,12 @@ "暂停" "语音消息,时长:%1$s,当前位置:%2$s" "PIN 字段" + "已钉住的位置" "播放" "播放速度" "投票" "投票已结束" - "QR 码" + "二维码" "使用 %1$s 回应" "使用其他表情符号回应" "%1$s 和 %2$s 已读" @@ -43,10 +44,12 @@ "移除表情符号%1$s" "房间头像" "发送文件" + "发送方位置" "限时操作,您有一分钟的时间来验证" "显示密码" "开始通话" - "墓碑聊天室" + "发起语音通话" + "已封存的聊天室" "用户头像" "用户菜单" "查看头像" @@ -122,7 +125,7 @@ "下一步" "否" "以后再说" - "好" + "确定" "打开上下文菜单" "打开设置" "用其他方式打开" @@ -154,10 +157,11 @@ "发送语音消息" "分享" "分享链接" + "共享实时位置" "显示" "再次登录" - "登出" - "仍然登出" + "删除此设备" + "仍要删除此设备" "跳过" "开始" "开始聊天" @@ -185,7 +189,7 @@ "一张图片" "分析" "你离开了聊天室" - "您已被注销当前会话" + "您已退出会话" "外观" "音频" "测试版" @@ -267,6 +271,7 @@ "离线" "开源许可证" "或" + "其他选项" "密码" "用户" "固定链接" @@ -276,7 +281,7 @@ "请稍候……" "确定要结束这个投票吗?" "投票:%1$s" - "总票数: %1$s" + "总票数:%1$s" "结果将在投票结束后显示" "%d 票" @@ -358,7 +363,7 @@ "无法解密" "从不安全的设备发送" "无权访问此消息" - "发送者的已验证身份已重置" + "发送者的已验证数字身份已重置" "无法向部分用户发送邀请。" "无法发送邀请" "解锁" @@ -388,12 +393,13 @@ "%1$s (%2$s) 由于您当时不在聊天室内,系统已将消息共享给您。" "%1$s 由于您当时不在聊天室内,系统已将此消息共享给您。" "本聊天室已配置为允许新成员阅读历史记录。%1$s" - "%1$s的身份已重置。%2$s" - "%1$s %2$s 的身份已重置。%3$s" + "%1$s的数字身份已重置。%2$s" + "%1$s %2$s 的数字身份已重置。%3$s" "(%1$s)" - "%1$s 的身份已重置。" - "%1$s %2$s 的身份已重置。%3$s" + "%1$s 的数字身份已重置。" + "%1$s %2$s 的数字身份已重置。%3$s" "撤回验证" + "允许访问" "链接 %1$s 将跳转至外部网站 %2$s 确定要继续吗?" @@ -423,6 +429,7 @@ "%1$s 无法访问您的位置,请稍后再试。" "无法上传语音消息。" "该房间已不存在或邀请已失效。" + "请开启 GPS 以使用基于位置的功能。" "找不到消息" "%1$s 没有权限访问您的位置。您可以在设置中启用位置权限。" "%1$s 没有权限访问您的位置。在下方启用位置权限。" @@ -442,17 +449,18 @@ "移除%1$s" "设置" "选择媒体失败,请重试。" + "欢迎回来" "按下消息并选择 “%1$s” 将其包含在此处。" "固定重要消息,以便轻松发现它们" "%1$d 置顶消息" "置顶消息" - "您将要转到您的%1$s帐户来重置您的身份信息。之后,您将被带回该应用。" - "无法确认?请前往您的帐户重置您的身份。" + "您将要转到您的%1$s帐户来重置您的数字身份。之后,您将被带回该应用。" + "无法确认?请前往您的帐户重置您的数字身份。" "撤回验证并发送" "您可以撤回验证并仍然发送此消息;也可以暂时取消验证,在重新验证 %1$s 后重试。" - "您的消息未发送,因为%1$s的已验证身份已被重置" + "您的消息未发送,因为%1$s的已验证数字身份已被重置" "仍然发送消息" "%1$s 正在使用一个或多个未经验证的设备。您还是可以继续发送信息;也可以暂时取消,等 %2$s 验证了所有设备后重试。" "您的消息未发送,因为%1$s尚未验证所有设备" @@ -475,13 +483,16 @@ "在 Apple Maps 中打开" "在 Google Maps 中打开" "在 OpenStreetMap 中打开" - "分享这个位置" + "分享选定的位置" + "共享选项" "您创建或加入的空间。" "%1$s • %2$s" "创建空间以组织聊天室" "%1$s空间" "空间" - "消息未发送,因为%1$s的已验证身份已被重置。" + "共享于%1$s" + "在地图上" + "消息未发送,因为%1$s的已验证数字身份已被重置。" "消息未发送,因为%1$s尚未验证所有设备。" "消息未发送,因为您有尚未验证的设备。" "位置" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 98a8aa1862..f91e3a85b0 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -50,6 +50,7 @@ "Time limited action required, you have one minute to verify" "Show password" "Start a call" + "Start a video call" "Start a voice call" "Tombstoned room" "User avatar" @@ -374,6 +375,7 @@ Reason: %1$s." "Text" "Third-party notices" "Thread" + "Threads" "Topic" "What is this room about?" "Unable to decrypt" @@ -466,6 +468,14 @@ Are you sure you want to continue?" "Remove %1$s" "Settings" "Failed selecting media, please try again." + "Open Element Classic" + "Open Element Classic on your device" + "Go to Settings > Security & Privacy" + "In Cryptography keys management, select Encrypted messages recovery" + "Follow the instructions to enable your key storage" + "Come back to %1$s" + "Enable your key storage before proceeding to %1$s" + "Checking account" "Welcome back" "Press on a message and choose “%1$s” to include here." "Pin important messages so that they can be easily discovered" @@ -490,6 +500,7 @@ Are you sure you want to continue?" "Message in %1$s" "Expand" "Reduce" + "Sharing live location" "Already viewing this room!" "%1$s of %2$s" "%1$s Pinned messages" diff --git a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt index 5df259fb5b..fad530a3e8 100644 --- a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt +++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt @@ -32,6 +32,9 @@ class DefaultVoiceMessageMediaRepoTest { val repo = createDefaultVoiceMessageMediaRepo( temporaryFolder = temporaryFolder, matrixMediaLoader = matrixMediaLoader, + mxcUri2FilePathResult = { + "matrix.org/1234567890abcdefg" + }, ) repo.getMediaFile().let { result -> @@ -76,6 +79,9 @@ class DefaultVoiceMessageMediaRepoTest { val repo = createDefaultVoiceMessageMediaRepo( temporaryFolder = temporaryFolder, matrixMediaLoader = matrixMediaLoader, + mxcUri2FilePathResult = { + "matrix.org/1234567890abcdefg" + }, ) repo.getMediaFile().let { result -> @@ -98,6 +104,9 @@ class DefaultVoiceMessageMediaRepoTest { val repo = createDefaultVoiceMessageMediaRepo( temporaryFolder = temporaryFolder, matrixMediaLoader = matrixMediaLoader, + mxcUri2FilePathResult = { + "matrix.org/1234567890abcdefg" + }, ) repo.getMediaFile().let { result -> @@ -128,10 +137,13 @@ class DefaultVoiceMessageMediaRepoTest { private fun createDefaultVoiceMessageMediaRepo( temporaryFolder: TemporaryFolder, matrixMediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), + mxcUri2FilePathResult: (String) -> String? = { null }, mxcUri: String = MXC_URI, ) = DefaultVoiceMessageMediaRepo( cacheDir = temporaryFolder.root, - mxcTools = FakeMxcTools(), + mxcTools = FakeMxcTools( + mxcUri2FilePathResult = mxcUri2FilePathResult, + ), matrixMediaLoader = matrixMediaLoader, mediaSource = MediaSource( url = mxcUri, diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 618587d2bd..93e92b81ce 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -39,13 +39,13 @@ private const val versionYear = 26 * Month of the version on 2 digits. Value must be in [1,12]. * Do not update this value. it is updated by the release script. */ -private const val versionMonth = 3 +private const val versionMonth = 4 /** * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 4 +private const val versionReleaseNumber = 3 object Versions { /** diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index e7cc47d7b8..ce5c324ff4 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -49,6 +49,7 @@ fun DependencyHandlerScope.testCommonDependencies( testImplementation(libs.test.arch.core) testImplementation(libs.test.junit) testImplementation(libs.test.mockk) + testImplementation(libs.test.parameter.injector) testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) @@ -106,6 +107,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:session-storage:impl")) implementation(project(":libraries:mediapickers:impl")) implementation(project(":libraries:mediaupload:impl")) + implementation(project(":libraries:slashcommands:impl")) implementation(project(":libraries:usersearch:impl")) implementation(project(":libraries:textcomposer:impl")) implementation(project(":libraries:accountselect:impl")) diff --git a/plugins/src/main/kotlin/extension/locales.kt b/plugins/src/main/kotlin/extension/locales.kt index 1af7146681..650fbc4d72 100644 --- a/plugins/src/main/kotlin/extension/locales.kt +++ b/plugins/src/main/kotlin/extension/locales.kt @@ -22,6 +22,7 @@ val locales = setOf( "hu", "in", "it", + "ja", "ka", "ko", "lt", @@ -38,6 +39,7 @@ val locales = setOf( "uk", "ur", "uz", + "vi", "zh-rCN", "zh-rTW", ) diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png index 6c0d8d5eaa..f2b02ca79e 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c1196cd5631cae5cb944cd0cc6903c4758a8e331983b2bbd93500473733f004 -size 34294 +oid sha256:03d7fd58010b5eb1bdefef76648e08431728f51527815db9df58ee0010f871da +size 34304 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png index fc9aeb0fa7..cf7fd18f5d 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b724d4087e37920bcb0ecf7812248f1dc31294da738866fed3000c63a4ec329 -size 36263 +oid sha256:bce8b45b24c38b3a991dffd06d1b2f79da728820ab93f649138ae071883aff60 +size 36269 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png index 6722d77789..38a62af758 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c609a2f5a80242c1fa9fc20d6f6295a85a23e1795a2603c64bce035abc8da05 -size 45651 +oid sha256:241dd64dd76b17b3a024622963c809f8788e0c5ef5f841b9be500942e4618076 +size 45660 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png index c1910fa5ae..5b6e58f735 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:831db915b860b285a9548e6469faa4d33445d7f79b28f6b54029bfdf2d61b4c8 -size 46567 +oid sha256:24ce85ea23acfd4d062785c013f5b1faa96d6ab75c1bab242bf03b6e6e330b78 +size 46572 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png index 9a66f5b538..100e2139d3 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0c96581bc29034a47a345b5c5f215853ab5d5545b2d64124031fb930bbd9f64 -size 48063 +oid sha256:e36b034ca57ca657072a089fcb7f7cccd2a5ec9897f80077bba29f85f2dcc96c +size 48069 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png index 6722d77789..38a62af758 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c609a2f5a80242c1fa9fc20d6f6295a85a23e1795a2603c64bce035abc8da05 -size 45651 +oid sha256:241dd64dd76b17b3a024622963c809f8788e0c5ef5f841b9be500942e4618076 +size 45660 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png index 7dc00e9c6e..2e87baac06 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b79450f73b04b2cb5a6cb674f303bba6326303ecca06f6a065112bc3b3e01db -size 46700 +oid sha256:0b1b8c43a078f16dd5424960c63bc42524ce73d5198834b5291f9bd874ee7b0a +size 46711 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png index ce5528cf17..5f28babc08 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44fa3cfedf5f3b943fbc7ccb8a7e88163dd97d7a314f1a38e339658bfd84f741 -size 39853 +oid sha256:d25872cb05d6d9b0cca7c579f67e531a059251ed033b3fa10a8969d161c9b050 +size 39859 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png index 4a0d1f93b2..81083ef1a9 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60f136a8ac494703edfe2f727e22befd07ffced587621ceb8b39136c3025fc4c -size 41511 +oid sha256:d2659371297c436c5aea18011221c081ec94299ba6a5643f5fda0214372b3096 +size 41515 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png index da5814af23..9ba16e11d1 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0c93f1419921ce124a015b3e6ca8dbed714f0e8eb7aa55d2e33e5fde2396ef7 -size 35296 +oid sha256:1845ce37a3c2cc763b745caeb6cfbc094b6bd65a3c8234dc8173a13c5377e038 +size 35299 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png index b3f0b141e5..7538d489f0 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:297914ef0da135e7294bb7bc476f581f2f28cb8cf4d8756b6d1379d1893b5c6c +oid sha256:abcd7267209f3ca0881cfbe3c9457f024d4368a1c8fc6bba8a00ab89e95ffac0 size 37563 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png index 8aa5a5b419..0608a5a46e 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e2bfad23495d22ef4ccc316efb4af24de9ee26731a39dc1cb665bfa7b1900de -size 47303 +oid sha256:099f4b603ee07216e66a75d5d08b8f17084648526ac14e4e611c7c090260b8fc +size 47304 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png index d1cc642217..62300fdbf1 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aec7bbe40f667e33c901290a357c0bdd35042b165e202f9f865d67587b38cde3 +oid sha256:dc0b352e7c81e7e6389985c4c29d1a7074bd733e56789f3f675bbecd6ff17be7 size 48289 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png index b261d63af0..fc8a1452d4 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68cf296625cf15e2fcef1a02fced8e68e3357af7211d66b7731ea198fa4c3440 +oid sha256:800381effd5dcdd3f41d28defd29cb21c99d254de86a91c1e7f60f476a4e393c size 49863 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png index 8aa5a5b419..0608a5a46e 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e2bfad23495d22ef4ccc316efb4af24de9ee26731a39dc1cb665bfa7b1900de -size 47303 +oid sha256:099f4b603ee07216e66a75d5d08b8f17084648526ac14e4e611c7c090260b8fc +size 47304 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png index 1e693e765d..b47aaf7358 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e41f2e18d265c3ba444041b286dca3e1973e861469d53f3f0933ccbfcdea5c5d -size 48394 +oid sha256:79db769c4c524aa2831ddf2e714102c0f1a5de1a7855025e67598c8f261e65da +size 48388 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png index 5f5d5a50f7..84c90cefef 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b3e812737af7688f1cc3ac6e251a0fc56b631754888760f960be8cf1174c24b -size 41186 +oid sha256:631556267b57adf739438e7a9c094c411273de3d74113216064268926499d292 +size 41188 diff --git a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png index c5c4572551..13ddf126ee 100644 --- a/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:454047441aedaf4eb85fd379acdbd0b9618ed348bc2ab876d92b963bc6ea34f0 -size 43106 +oid sha256:d8c0999d16e140465ab18fff34650ac850cf650316f52b4a3dd9145e6f928d04 +size 43107 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png index 57b0b95a3b..e7b089068f 100644 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77959d24fd7c52c4d63090e51d189bd6c41c9c350c1a4c97a2260c05b226c119 -size 86868 +oid sha256:e46c26ec7af7c4d1c1bafc0fb8bda6ef4afe7dcc6ceb8a5a38f2872ccafbc6d0 +size 86687 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png index 5b2f0bbb1f..0d394678ce 100644 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53a21b0c20ed1fb442fdb8d860805ee8f476cb2f527d571b07084751b58c762b -size 39239 +oid sha256:c50c374b59c957f4e007d9548c2e03ffb622fcba3de9bb0adb2e36336114263b +size 39053 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png index 5b2f0bbb1f..f0e44135ec 100644 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png +++ b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53a21b0c20ed1fb442fdb8d860805ee8f476cb2f527d571b07084751b58c762b -size 39239 +oid sha256:c7f97df762e010742fb2627a83e0939f4e190195200c53004412e01c6361cb43 +size 28480 diff --git a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_3_de.png b/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_3_de.png deleted file mode 100644 index f0e44135ec..0000000000 --- a/screenshots/de/features.home.impl.spaces_HomeSpacesView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c7f97df762e010742fb2627a83e0939f4e190195200c53004412e01c6361cb43 -size 28480 diff --git a/screenshots/de/features.home.impl_HomeView_Day_4_de.png b/screenshots/de/features.home.impl_HomeView_Day_4_de.png index f200461dc7..40500e7b72 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_4_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19266c73ca346e8a290c9eb4091cd5088b5569710a31d06353782be071c03fcb -size 58359 +oid sha256:55944dde4104ac2de65a11ab041d813416a21aae25e4cc21b9f624a2d33a80c0 +size 58447 diff --git a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png index 3730e14b8c..950394d6ef 100644 --- a/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png +++ b/screenshots/de/features.joinroom.impl_JoinRoomView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f80183fc0e3b5b06b51bcf58f4547111d3df1f800aec777fbab41d9d7eb51a24 -size 42252 +oid sha256:b9b94629caba74d3fa8f9d939edc441b687c6496001b185c6b7dc4ba13a197d4 +size 41978 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png deleted file mode 100644 index 787f11c872..0000000000 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6be1e5c5f4abfdae4e8c8e059ab6bd23808d014c76a029762f3bc213935322aa -size 19235 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png deleted file mode 100644 index cf4202ecdb..0000000000 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9666da59d98a5c1ea6c786d42d5f7ecdfacef41f795dd9481c8b207f00e5e92f -size 37893 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png deleted file mode 100644 index 43685df0a7..0000000000 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1db33582d79aee02c96c99e1c42149f91d5bc7a62d1b095428262b03619e1c4f -size 34219 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png deleted file mode 100644 index 787f11c872..0000000000 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6be1e5c5f4abfdae4e8c8e059ab6bd23808d014c76a029762f3bc213935322aa -size 19235 diff --git a/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png b/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png deleted file mode 100644 index eddcfdc5a4..0000000000 --- a/screenshots/de/features.location.impl.send_SendLocationView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bdbfd52bef468588d3655706a06c78d7c9fe6dab354a832b85236574d77af112 -size 19444 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_0_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_0_de.png new file mode 100644 index 0000000000..26a2d11771 --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e80b99b89c442b8126cf80c8497b524b03012075763dd34f646fa1f7850a5a25 +size 19956 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_1_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_1_de.png new file mode 100644 index 0000000000..e1f4b27424 --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94bc010e69f70262da8f693185bbb4e5ab65c1443ffb04409aad4b4ac6d6977d +size 38635 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_2_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_2_de.png new file mode 100644 index 0000000000..5b33246329 --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fcdaa31d82e5e677b39ccea11767febe8d51789d410e0934b07ce27ab4033f +size 35151 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_3_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_3_de.png new file mode 100644 index 0000000000..f4290dda6e --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2eb237f3bea51645310fe28e66a374ee7eea722d262261faf7a2d4ad7bfc9515 +size 30400 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_4_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_4_de.png new file mode 100644 index 0000000000..26a2d11771 --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e80b99b89c442b8126cf80c8497b524b03012075763dd34f646fa1f7850a5a25 +size 19956 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png new file mode 100644 index 0000000000..18dc2c74b7 --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6edd90a847c7fc84c25545313f90ec41b8769d4a9b758476725f7ec5255fc167 +size 21838 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_6_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_6_de.png new file mode 100644 index 0000000000..7967b79a1c --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f148e3b2e061cc9cc1e3a6568f4452175b51341a2c317aed2c52a355351cdff +size 42513 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png index 1e8329391e..d4d8b13367 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7023f8cfa5fcaf68d44790813287382df6b5f496e1c6bc3025f5e6f280b82f85 -size 11123 +oid sha256:c27b436e8284ef32e2d4fe1285587f1580b4139b6e098d9e37e976c98f595057 +size 19318 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png index 85da5bc99a..61d38bcadf 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4976c47e82967168892ef6a0125210b437aa5a2bd80f6100979a60ca365c259e -size 32420 +oid sha256:c0e8730caeac7c344ae3d7c605aa944a6b8d021b5f3b8cfd387cf9cc2814c6c9 +size 40339 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png index 53aa498533..f3502aceac 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1befcf4a87a8aa3c925e55d3ae8776627bad230cff5e09f6c8d33172749be313 -size 28793 +oid sha256:4b70b5cc45b554c282a435f59d5311483569ad6be9875f25a1b88e1c9119b264 +size 36812 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png index 1e8329391e..d1aa86b7c6 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7023f8cfa5fcaf68d44790813287382df6b5f496e1c6bc3025f5e6f280b82f85 -size 11123 +oid sha256:016809a14091abf8554648ea4cd45945541395772c540d8aa8b04a24c0190050 +size 32120 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png index 91776aecf1..d4d8b13367 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32b1ab1cc6703376e1649d5b4b0288b142d954d7cbe02fd966a523e2c7b01f43 -size 11254 +oid sha256:c27b436e8284ef32e2d4fe1285587f1580b4139b6e098d9e37e976c98f595057 +size 19318 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png index 5b2e833a43..a0d42acd55 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46d118957b387eff49efd968202186592087e32e01859b3c84f0f90b730e835a -size 14832 +oid sha256:10244806518a4f64985be17fa5814d1523ef8796398a02632d48bcdf2d9daf53 +size 19451 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png deleted file mode 100644 index 9942a70172..0000000000 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef794ae82eaeda53e7300b4c935d9ce7f4487b204ee78020bd4f518f816f7880 -size 23288 diff --git a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png b/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png deleted file mode 100644 index 02f54e7e52..0000000000 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d525359e7d0a5dc0dac54f05acf77dba55528bc3cfd614c047bccdd670267b67 -size 25989 diff --git a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png index b46483bee6..80e1c0e9f0 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0d3451e07ddc21d22d59ab932075b0b957f7a51903f373922193ade95afb43b -size 54648 +oid sha256:58932b4453c81582a8f3bd4e47a4dc37355bc65ce23413715d240212e44c5dbc +size 54567 diff --git a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png index fe5e6a62f3..aca8462ace 100644 --- a/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png +++ b/screenshots/de/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ad8ffbf4727694e35b4104a0329443e4087ebfd780a15c0b2680d14fb50376d -size 65291 +oid sha256:b3da0fcc021270fe7cfde72e03f44f9c95428f18673aa86bbfb11a6acc0b344f +size 65207 diff --git a/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png index 057548a09c..ee068ebc52 100644 --- a/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fd7dc09638aef0559bd4b1c75800648d9f37c67d96fde244eb064a4374c67e8 -size 21574 +oid sha256:a237e342ba5bce3980d0f0164afe658993741ff8238e7cbdf9c73f7eb83f2f4c +size 36792 diff --git a/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png index ae43fa0476..e7fbb21056 100644 --- a/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.messagecomposer_MessageComposerView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaef8876d48f81ff763ad24c6f4ae7aa2dd56e8968d75f2a1751c09805fd792 -size 18168 +oid sha256:ab856d0a2de029924cccaf9f2285f3178315d83fb12e02f7f71d9c69fdddc5d1 +size 17989 diff --git a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png index 68dcee55c3..807e16c40a 100644 --- a/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4b1981984e8edde71723dd40849471b514d101b3d545b0a6132bea1cda0358f -size 43592 +oid sha256:bbdd0d31786b29709c02e9f35191af1334b6f6a32cc0614a833f79d47551fc70 +size 42687 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png index bc903778fd..36358041e6 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:023e811a9fa6157440eb00ca584988273efc2f37ed3804208bf5aae8b9136673 -size 364436 +oid sha256:4f44887b6c1d8c3de4e1bc4ae42f3999b99db906062ba95df22c88a2951f10d1 +size 364996 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png index 628521bd47..dd1594478c 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6257478d8e46e942ca037b1aa178477e188edb9777d67a3e7560929d6aba8880 -size 370034 +oid sha256:335e62d5380a9f6eba40017ed5f8bcfde6bacc7aaf29995852bf2c9c38960a09 +size 370563 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png index 792aceaf60..6f4a81b5bb 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc5e553875d40920c9a9066c25ba6094238a7894aeb26ab144ee4cd9083f1754 -size 363641 +oid sha256:6b6a749b9aefbf37b9bb4d9edcbf91f857acc395344a58439079268c73eb173f +size 364271 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png index 0053487297..bce9fe091c 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1fb4257bed537387bda20ecb17ded43f4f9edfa1c3280b58fdd8a3dc9c91819 -size 365658 +oid sha256:caf04029d097a3fcd937ecee55381b15353ca70b180629f3cc6b35dcde671c45 +size 366135 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png new file mode 100644 index 0000000000..ff53dce724 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ebbbddf29df78c6a6b37f931052cac67ea7ae1c875e2d75fa6ae8aa1683971b +size 87348 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png index ff53dce724..23715fd683 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ebbbddf29df78c6a6b37f931052cac67ea7ae1c875e2d75fa6ae8aa1683971b -size 87348 +oid sha256:0914e81c505541a4f000240469e746964c5f476a7884403616015fab62f23663 +size 51743 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png index 23715fd683..44e66cdc1e 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0914e81c505541a4f000240469e746964c5f476a7884403616015fab62f23663 -size 51743 +oid sha256:fa15ce3aa51c83819e6fbdf235e6f732b0db187ea49344279b01614efd9ff6a3 +size 63456 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png index 44e66cdc1e..04e605318e 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa15ce3aa51c83819e6fbdf235e6f732b0db187ea49344279b01614efd9ff6a3 -size 63456 +oid sha256:961a9289be61a16372087c645302e4043d71f73c4440e24473941a9a6aa41daa +size 48104 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png index 04e605318e..e6bbd0bdff 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:961a9289be61a16372087c645302e4043d71f73c4440e24473941a9a6aa41daa -size 48104 +oid sha256:fb03835fff5c4d27009fd19af5494b71501445184f6cb984e39fa175e430fbbc +size 72082 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png index e6bbd0bdff..7169b684d9 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb03835fff5c4d27009fd19af5494b71501445184f6cb984e39fa175e430fbbc -size 72082 +oid sha256:f9bb51eda6e69e8cd532a3adebbd33267580fdccc904c91e741cb2e8026f0b79 +size 57796 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png index 7169b684d9..c3563a3472 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9bb51eda6e69e8cd532a3adebbd33267580fdccc904c91e741cb2e8026f0b79 -size 57796 +oid sha256:5b88d255f070df4fad03371c64ce31e080a35999352a6a87cc10f83d8d5da99c +size 64506 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png deleted file mode 100644 index c3563a3472..0000000000 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b88d255f070df4fad03371c64ce31e080a35999352a6a87cc10f83d8d5da99c -size 64506 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png index f63e3636a0..ba7d60941b 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b28fefedd7a34cb758217de7b240b536abf6f485bd256fd718ebc181df45e15b -size 70537 +oid sha256:e916a49d3cd4d81d8ec5f3d66f0b930bfba6e9c20f1b9c5f7d4c0bfe51895036 +size 67398 diff --git a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png index a624c779b4..0d70a656c2 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa1490b35cdd22c0b01db869e8bad73195481fa2a2e7e61eb289e3f51bfc6200 -size 73417 +oid sha256:86fa2e5742ba38c173add0921615a18c1271caa783fd1a505c80a06096a3c248 +size 71646 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png index b3df3736a3..6a6d9271cc 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddcd24501a9e91f4a42cbffd60693a93d2dd0c2bf28e469b1b4ea460e2630d17 -size 67293 +oid sha256:d5c26f5383e1d079f5fa8a62baff668032181568a52bbbbc90bc2ee5d70d78a7 +size 67208 diff --git a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_de.png b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_de.png index f085e23212..9fc37666a6 100644 --- a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c823b266179d70fc5b6b3a3b1e0654536d1971fbe615f9db99fc8203049bf5b -size 23526 +oid sha256:2fc98a42e6af1c52bcd185592680871187a5cc942de9eaac694c0209f50a0c64 +size 22678 diff --git a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png index 52c0695203..2144c61e2d 100644 --- a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png +++ b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6c6e0585e05d474d7797176d1f8222654d1eea1522bf508b353bf6b5694eaed -size 65127 +oid sha256:6f493c136e7284783a35253f88a752ed973d628786e7c0db037a8c3a577ef8a3 +size 69694 diff --git a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_de.png b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_de.png index 293b43a7e4..0e09e3da59 100644 --- a/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_de.png +++ b/screenshots/de/features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06b30b3ffe792671b4a68044fb8b4c0deb9f1a0c0e121f166e2e4c50430f6c98 -size 35065 +oid sha256:f1394337ff80f74e9f4b6c5c2c947db462677f2957e0b73d2e919aabb6d2fc99 +size 34412 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png index c2ae0556f5..e38435bab5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ba073d0fe9e1119e008a086fda234bc3451df4685a4d4704cf8721244f92e10 -size 46184 +oid sha256:17ad93c86555868ac098ce3beadb8dbde57c89af9bc2caec61df1f3596325ae4 +size 46178 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png index 6d58ebeeb9..23fc5ecd1f 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:723d8bdb06037c0098aa35fa2a8300a9ecee7367b5ee608bef7216dac4f46d91 -size 44761 +oid sha256:d16ff2356729e9d5aea99f00b110efff09d6c0beaa33e100846f3c56cb0fb0a9 +size 44757 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png index dc74aeddda..fa145bb3cd 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9864cca2481e1f2f02ee625fbd47f3a182fa997cc917bc842ed893c1d2eaa151 -size 43560 +oid sha256:e8c48700213f1ac24b20c08d8585bea693ae08cf4a3dc91e6ac6aa08c1a02cb4 +size 43518 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png index 2630d505bb..564d0ae6bc 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1aca90256973d9614b6dbbeb665b782ebf3c59922c3fd569552d58d8437f54d8 -size 45347 +oid sha256:6f97df43c4f6e479e7ccdaa94d0e817f2ab3f02cf21986df1af09805eed8bd59 +size 45340 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png index dca83692da..90d69eb98c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb22c1fa6e30e4fa2f9500fd33f3aa44bab2c2a1eb1374b306a216a808c96192 -size 45260 +oid sha256:75b4afed84d26d914eb82cdef4d9b7757d7cb27b0ee938c2bcca48d10553ced9 +size 45253 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png index ad0dceff1c..85c3b70cf2 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d933933bb693c3e227a750d5db76afc227df9ee4932823c6aafe99ec98e6dc1 -size 45838 +oid sha256:b7d85799138c9367a1229f36d90481577f08e027c4e946789f48e098b3927a0e +size 45832 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png index 82316c99e6..133d94e153 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a6f8f86d9a770a893949c7bc1762e3e05c6940be58708f5ac7a1704ca84debf -size 46369 +oid sha256:3ffbe5ab44b8cd51b94d766ff8bf0cfe05b56152d94a75d2858aca6b25db0bb8 +size 46364 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png index b70917c986..95a9128cc5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5978469c7701bfcdb1c4f5af0279ccd712c1c9a7ac17d26751659d0bd72fc7b -size 45626 +oid sha256:18f4ef21e7a652f29dc88206e514ad600c05cc9dd2a6a3532e4aa3e68894d7ba +size 45621 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png index a6ac629dd2..eee91513e2 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b81a4f9a425c64ff9d3bbe5f61fc9abf46d0c4d5f746aa8b35140dd7ad568a6 -size 44844 +oid sha256:d04a9c799ef61bf131131e527c8734dd8ca67880d92a9ad121b576bc11d4a992 +size 44835 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png index 6d2de679d3..d99db83de0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8608f7c43d21b688f60f7416f144df912fb7f8852949cd9fdc6752b47f9f0338 -size 42918 +oid sha256:cd41d6b3943d4c5e74090acb90a712b930be74c49c6a39fbe2f3b60e90855fd0 +size 42886 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png index b1448746d5..d02979f393 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a9bba827ce7181ac459aa41f933bcb1513a8adec101b89a7557b4759f1740fe -size 42871 +oid sha256:17e33f973d9e33f6e06e7b2b66d448d66183a353ed76c1d7712a13e2af9cbbc4 +size 42839 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png index 956fc80005..e0520466b7 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:941b9293c96017d16e5964bf13f5b6b7954af6588a8dce2a8f731bcc3efe0321 +oid sha256:ccc14ef7b66c52afcb6d806b4d8d170ffb83b7817a5ecfaffd706853bbc35cf3 size 41743 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png index cdf129e144..d8814411f0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8dc02ed20454ffade7a393956c0dcbb6f15a7d7012c0322cc43291580ffdc99f -size 47790 +oid sha256:33521284c24815367a302bde9c49b2795ecb5d803ac322220f720c5269605ea8 +size 47818 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png index 4392889de4..e89ffa97bd 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eca6cd07f5eab788aeb07a35fb23c8f4ffc316aa9dbd1b48efdf00d16e6b4093 -size 47567 +oid sha256:4f16ad28caae15b4bd13fa61dd5b95450a559b00dc005b157fecf44c9b7df625 +size 47553 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png index 9f391af648..3ab82edebb 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:450d628e5540ec8234d6c3aa33cd657ff51a56b5518004eef628eab92328b616 -size 47221 +oid sha256:5c493224b3437d144e064f91a9dcc97779953cddd81aa5884631ca0046d72dad +size 47264 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png index e53d7fab40..15c3785853 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6411a8af9386bfca9b171e16b60231bc51c0a7ed92ff462918a1342251ea65e0 -size 39459 +oid sha256:6ab93cdb57a580a52ed48dde779355440d6a67412f9699927a8d4214388fc4a0 +size 39451 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png index cee674d4a0..ac8ea4aa9b 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:731fc476ed64dcf665ccc999072be646b04b58d7e440ba96e38cddf6953f08a4 -size 45410 +oid sha256:2d1c6f0c3898e000774de48cd7590c44bc11c297163735f16cc2dccaaf23e538 +size 45428 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png index 8cdd7726b9..85b757fb86 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53aea24a421f5c5bdb2b4b0cdaa3b73f45c2747abc2d547c52b35234bf1944ae -size 44602 +oid sha256:647eaeb0c9ddce71bf99e382fa45bfc560affe38e8581e12016a1e5a37e270ba +size 44587 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png index f9a596640a..ddac91c4b5 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fbb251010c11e7cf41cd9d772628eec858e2eb7eb102f0d07a177f25d7e0e42 -size 42559 +oid sha256:2b129c0c7e62d3874d1b7e466dbbca271873b20d3155981eafdbd9c33202eb04 +size 42527 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png index 346aa30d3d..a4d831a49c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a898591cee5b687baf9ec6d1c4fa1c0de34b99e45ddc1541de15d468c1ea1b1 -size 45985 +oid sha256:35aab3535b161bb3b111a9a352f510b43a25f9c98a7e5648aa3a380cd8402063 +size 45969 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png index 39f289f1e2..a85951f6d6 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d62137742d97ebd6f1ca605f8239808d063db2a5af5a217fafa9156a7d8906 -size 46085 +oid sha256:eb393ca334e520249176e392cbf9f495c8651904f2e03680bd740f4d9b1c1ade +size 46076 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png index 064953e54c..d8ac60f111 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bbc17b6609505a390fe0baae91706366deeb8bbb41bfe8493e98e23a1753c28 -size 45479 +oid sha256:6d420ac1d49b4f2a47d00e6fc273e2f2768b14a05f0d10c8c655cb492abfe142 +size 45474 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png index 77fe390317..3ec8b090c2 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f9836b0fdd3bda742e2b4c7aa7f724f1d6dbefb748d4ed31ba6912ab6600f2b -size 44166 +oid sha256:8435264c98cefccc80e7a4bd32d903708e878f4c4ad2686aa35df8fdb9324882 +size 44159 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png index 6fd394851a..8d470aa1eb 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f006c30451416d0a41c3d902f7c9dad8b4d404326e623534ac7a09fe5b2699f4 -size 47231 +oid sha256:c0cdc554f6e3ce1725e2459ca0c6133f7f14a15d730f190bbbb795eb11c0404e +size 47224 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png index 3346f21554..dc8f2611d0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28e1fe354a3305ad933daa95a65611335357ceb6599484e85ee4d3ce2f019e79 -size 45747 +oid sha256:d408175ca51c0b36ec01cd950b079467e29bdfdec9b18a1b81f1480ffff51e6b +size 45740 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png index 8028d785f4..5e1712b9a9 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f7d823c76b57b8d37cd86bf6dde6e61092dadeb03f65e0fdb3d9d0e7b016513 -size 44564 +oid sha256:3bc00b126e3820666bad9a4904b3956ab391a6af0f74ce30794373af1c654ea8 +size 44592 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png index eba1696597..6f725c591f 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15ec7bd1f0e7baa884e7e89aadc485a7ab02d80fdc2957c5ae10ad6369f3bbab -size 46376 +oid sha256:438edecdc96de067dfef71395ca0dbde45b5127f5a8d507dd4148f43da12f1c2 +size 46370 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png index c9813ac9c7..db3edbd7ed 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1818053ea53eb07fb96a27350bb0e59c8b696dc0a34ae16f7b814743d377f06a -size 46272 +oid sha256:2272a4633116a8f58a24273fe348e4cf979eb31fa78a641ea6da28345e358846 +size 46265 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png index aebb284744..8d78460895 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:966dd5700a19d89bb8192b153f381387e80094dab940fa876a4c4eafa73c23df -size 46776 +oid sha256:a45ea374ed7827a1aaddcb6b5d0eb344b4c0d79ae328099bdff79a8a3ef84b5d +size 46769 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png index 74cd663399..f7d40446e8 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba4a4082698d5aba2e36ed11e50df97b59c2d85e09b53b1e3d534e2dc97c34e -size 47377 +oid sha256:d4ba031a35b259142083b6fd508b7ab15cf169f7f5b1535bd81ef7e4fbdc0a6d +size 47370 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png index 03fa90e4ca..fe75d48f6a 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d258cc980ddde7603d07c1d0ede66eb05a61215e8a9f7591083f44e3fac48ce3 -size 46652 +oid sha256:7e87581df1ada41c9b7b85f576da8c32f856203a6c48e7f40128fbf30c7a33a0 +size 46646 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png index 31d3ff96f5..56080a7551 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e1cd8ae0035eb0cda747eac54bd11e3d95b8f006e10c05752209b0aff5062e1 -size 46149 +oid sha256:d87b127824291e0343f53cf50a22f0e1e8b89e00be55ea5f625aac59f9109a4d +size 46142 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png index f8dfb9f0c9..e9d5014602 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc96bea99a5bf31bb0f742383cba458f14baf150845a47e1d14681c806c2021c -size 43913 +oid sha256:827747ea2eae177d460d615b9da37db515f635ed433567945e1d88cb39337594 +size 43944 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png index 16ce3617ac..8bf44cdefc 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b6d30a4661310b3de30ed34c55bbe37af245ccd0c363cf2b5fea7c66f058e4a -size 43792 +oid sha256:1765877cdc96b174cb7a2e087165c74d1ceca02137d0daa510ec247cc4f481b6 +size 43823 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png index 6de030abb8..52c52e8769 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5327a706c81ac518776c5ad183bf601c14c97ea7bebc9b7c52cd04a2919e40ed -size 42899 +oid sha256:6f3c5533e239f54302eb7867bda7571fd966b4c298be9ec48abad6a5025c22d7 +size 42890 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png index bb4aea59b1..c510c5f777 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f989780ab77db2fe17b0e7c52f8d73ad9de1c336bb02b4dc29140a4cda55cd58 -size 48860 +oid sha256:abb0b25a08a6507a9b3c0f3ed79ca11b26dcc54a9350a0711f9385ccb0ffa29a +size 48855 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png index 84672b20a9..aadf561762 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb5e80f50cfcf6e6eac762a3bde89bf474931ac8a10c951d480f354f6bf81875 -size 48581 +oid sha256:f615089fb33bf68a1f24bc1d7f78ba899ea80c5dd5e046b05df04055e7d15ed3 +size 48571 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png index c8ed3fd3a9..2c22f9b92c 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aadbf1e409f2837387ea390c82ec8bf16a979e995d01bc0250cdf9d4bc7c881f -size 48252 +oid sha256:c9f1b2f0372b538dfc219ac8ac699347a199f0df903813ad1a97b7e158f50f9f +size 48241 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png index 83868afa29..22634ff9dc 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ffc68081fe2c9c391f71635a160559d4827f5ce69589d799ed499612e77260f -size 40469 +oid sha256:ab5386c966f4b5981bee9d39cd6a315bdc86dd286ff2c5e08aca4c5bb3304b23 +size 40457 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png index 7daf7e374e..95c8bffb80 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2fe91f0d2dc898b39893d1efb70a827197e59af026fe53badbd4b1dd9a7ad11 -size 46398 +oid sha256:39e4eab91f3d07daff1437991ec74005e7c79eed3f9b4ff71c154d68bbce3e71 +size 46400 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png index 69515bc17a..7574b9b61d 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3388b6b6f46a7c90f3c45568e22d7d795f91a8a5e3fc9fabe7acfdd99013f868 -size 45606 +oid sha256:49009a3a070a668b7487ce69b3bf1377ce113cf003e1571db3f90d8319ec084c +size 45597 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png index de53328c9b..00e31d8cd9 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8cec2bc057e632943ba3aa42fa427ad6f6e766682903d620067ab5c8437ef21 -size 43449 +oid sha256:9e268c85951e089414695eed071ec5c98a80030d130f92d8a934f815dcc75334 +size 43480 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png index 6620a09ccd..71787b6f1f 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a84cc9c31168887d8924044df0e558b4aeac360a52426027be5b5c1a020f116 -size 47082 +oid sha256:eb164e2c9796a1d12a0146b3fb67d29813be54d42b0628d17a323a525b866177 +size 47065 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png index e1ed2068e2..ed016c1eea 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65b9e404e0cbc2315d595febce884f11034ef7119ca76ae9dccbab4d2248bc96 -size 47259 +oid sha256:5a0b75e17e467d1a8abcb59f0e490c57e1bc538f10a6900cbff2e2c14994f0f8 +size 47246 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png index b0c9775ea3..d4eb3afff0 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36cb6dee1515ad0239cd87969649d39ca53b528651565ce9f49461c6f7f12198 -size 46633 +oid sha256:cd5c0dbd3621d8936b4e7eaac29524fb837007daef330802d71a12c1978d973c +size 46623 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png index 472623e81b..3466e4571a 100644 --- a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png +++ b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6436222472999c9ce19fdab8e5ee9d10dbf846b2764a366826cb4150c0dd426 -size 45170 +oid sha256:706e54694211c141ad5cd505379f695504b2fa4b793a9aae5242d52f72b9aea8 +size 45157 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_de.png index 547b9ac6d2..8c69f2926a 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c96373fa4553adb41fd650ddd7794addd57a2c007152bcd24a33a2677f692aa -size 29729 +oid sha256:efc41570c6e1c04f96a4226b629a55fbc979555cddc20e62659b57e9e9a516c6 +size 29702 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_de.png index 830a59bc98..f3f3f717c5 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5acabb4f45503b9c685510dfae0ebffee07b6383d7471edc74a462bab952ed56 -size 24105 +oid sha256:420cffbe21d04b2e274cd5c6d24076f05a21081701a60925f7efe0aa64f82839 +size 24097 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_de.png index d4f4218c6f..e963ada9fb 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1331106fd5cdb17e1a64517582e2017942bd30981e35c9104ed9691dad6f535 -size 30220 +oid sha256:2284ea6f41d947283f997d48c2532119c1562b606bdbf730bdba4755e67c28e8 +size 30135 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_de.png index b9ce13764e..f1100ff0ed 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a0fae6a3d68c058cfe4cc4d0a7c395129262050f4c423dff5734318b6d5a5d9 -size 52439 +oid sha256:582e106bc388c89ad644b2899390e82e5740004061aec622b6b97aa1bac12f85 +size 49429 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_de.png index 845659c354..0888d9e355 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fb81d71260f2ac8568d8505f4ebfec1ad97e8001a94db6815e9dde859a38e30 -size 48491 +oid sha256:cca14f96f4ba493ce291f9f982d47a84d6eea3090e774bc4f2b1df63472db696 +size 76461 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_de.png index 43681f14c0..b54c41f267 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d3e9968441f6189ec0e234c34178f7802ea4da778000338da3506ee428182ef -size 29869 +oid sha256:45bd842b590d153fd7c727b08e34f69c73fba0f2c73530f7726efae1386b37c5 +size 29846 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_de.png index a2a0734703..5e4641cef0 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1067316ebeedc49b187fec61da538e07431612d5b38efcf8f7360023d4c34c1 -size 27968 +oid sha256:4a1d1b889880d3e4065b7de036c7e6a26f6817dd6268df957d948ff06aaa8c7e +size 27881 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_de.png index 698dc47f17..9f16c22be6 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f1cb7f8790e0bd8ca61e137494db81295d6bf3fd6eec0f04983178a17c2bbe6 -size 25608 +oid sha256:45c2edc8bc542bd02c26c696c339855f8176afa349f7097f35cd9b5d6a45ca43 +size 25889 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_de.png index 528eed4126..440f2a693a 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91592dced46d59ecbd6edb157ddb5325fb6a8a495a9434c2d878763f317dd104 -size 30930 +oid sha256:2f74a6693ce54a06ddb795f4787252cc35ae457eeb71330e2c3522aad64c55ee +size 30928 diff --git a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_de.png b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_de.png index 3f716d49eb..13f81454c3 100644 --- a/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_de.png +++ b/screenshots/de/features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6d39446fb8d8889b01bb411d92022fcf80e04efa89fd9bee7c071de791f8213 -size 32436 +oid sha256:dc4d09a07b88320eaa41e006cd67c56a7e6d5459f15e0471b00b0cebf73f95c5 +size 32558 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_14_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_14_de.png index 51dccd31d9..2e8f128b7c 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_14_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_14_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8944a90ad9e5cd283b9a46547ffed79b932fc0fba9caa51a5d2e91021d302bd -size 66333 +oid sha256:4ff016cdc3ea47662f224c49fd182cc16ca3bb1e09191ccaf2c418ab93a358a6 +size 74302 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png index 1b0d3b202d..9ac2444e4f 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae0a43f1e5cd9dac74a4bd9783d7dcec09acd8d972b5ab24f6f7b1afd55d0db1 -size 50203 +oid sha256:f8804204dff0d36762b0c02e9503f2e06b6be257b9e1c0c2af6df0c0fe2855b9 +size 50289 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png index 853875b8ee..36817c5f75 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59af40c662f9dce190b7dea5b7ec52e69c6d7cc20d70ee9ea605ed730923f216 -size 50362 +oid sha256:a69c330592d208be768c7f607bede843483a92140155c974ad4a84116ebd142d +size 50380 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png index e1425f18f5..40e51c9537 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e648d7ad31cd4369f8230685a9e4a87b87116a9187502d2c224412d722d8bc75 -size 52276 +oid sha256:428725b17dde8ae30898299043e2d5e03ce63f516c93e440e660823fabd43e4a +size 52036 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png index c46b7d0dc5..806bb43ddc 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7683c2be445f411fb2f5c408db5ffa6e40ff562c9cee757ce16b36e0643aed0 -size 61299 +oid sha256:ccd76b6702435a24f67ab28adc60b462acb2fb430daf87b6ddc53237940bb29a +size 61987 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png index a02909f45b..75087f2a67 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0cb890656c62a8c0b5004a905d6614e89d6c395632b168f49047443c4853e62 -size 61924 +oid sha256:524f0d29e59175ceb996b81b98625e168033077b12541f8747fa6c6082097cdd +size 62629 diff --git a/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png b/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png index 8e0058a639..8a71650321 100644 --- a/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png +++ b/screenshots/de/features.space.impl.root_SpaceView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:208b78836148e8403ecfbaca6b3f14bbcec1756cb13ea0fbc00cdbb6c970659b -size 57796 +oid sha256:e338d699f2cb5d4645742e9dff3dc7cd3dec20d2ad75821bd3fdb3fb8e917a96 +size 57250 diff --git a/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png b/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png index aba971c8a4..b7ab99464e 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileHeaderSection_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f20ceee8793c045bcad1c1f3ce30d6aa36ba5e688c8f8b14161849bc0bedff01 -size 15889 +oid sha256:4b8a6428ae3624826edb3917a78e641df0704e8c21519ac003ff27387d0e37cd +size 15902 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png index 2a37678990..e7254790cf 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeea0d322e256f499ddda7dc6cd40c2163f75ac7b9755d75289dde47f28b7c3a -size 25016 +oid sha256:f7bf1f5e039bdff2ba1da2678d57637564288733b9958042b86867350277ca9f +size 25025 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png index e8a6cb04c3..6463867d7d 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d45be0f3300cc450129a656e28dd131c66e84fa36bf692a69441867d42ad562 -size 34110 +oid sha256:f85a57b51f5ac9d7165be8d83c414b8a1fce87f2bbbf4a345f145f01ba80e3f2 +size 34420 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png index 18a8311f9c..bf3926ade2 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f3d837f5d0d5899f0a20b71dd8f39c1bc304bbebfb1b34e48f7b2feab805133 -size 25895 +oid sha256:e050acd07312ef4e19b85323ec4c061f9320fde6291a2818e58771257caf777d +size 25803 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png index ca4eb19234..58f0d723a0 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070a29bc9f0e4a9148348eba4ed164be33f0a238b892caee66343ad43c16a6cb -size 25601 +oid sha256:cba578353fc5d7e9e9327b0ebc89b8aad9e3743014b3fffb93538bb89fb9026b +size 25479 diff --git a/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png index ac7f62e753..8c65dbd7b3 100644 --- a/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_OrganizationHeader_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df1613037f5b06b6cf9886056977d2ca269bbb09729d9aecdba9915c77cbed78 -size 41954 +oid sha256:0a8a0630b584a45d35879c3d729921abe6d810245cbf19ed74fec7d24c702d40 +size 41390 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png index a46fd0d98c..07b95441bd 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d26f94621d75228ea9bdee2abc8616b378a64cf8b63b7fb043f32fdef61082e3 -size 18046 +oid sha256:fcf290de055889a30b6859a0815655b07ee96e600e241ad18af768ba7d0691a2 +size 17777 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png index 90d4f208c7..47ae5e64e6 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceInfoRow_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1472df8a860358e2193de329ca363a85cede2c65dc51b7182545152f9caa036c -size 20165 +oid sha256:5f245cfa43b2310ab533005012debe347d196b4c177e696ff4399c8431397113 +size 18057 diff --git a/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_4_de.png b/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_4_de.png index 6a40ec4ecd..a0198d4d9e 100644 --- a/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_4_de.png +++ b/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d350923703149b79d86d1029b1f415eb0e90b69165956ae44a23acfba203aa0 -size 8839 +oid sha256:de86d0d2cfcb6c2fcdc1fa3ea89a31ccbdbeaad7068fb29992c2989e2f3cec20 +size 8789 diff --git a/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_8_de.png b/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_8_de.png index 97b55075c1..6e431b335b 100644 --- a/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_8_de.png +++ b/screenshots/de/libraries.matrix.ui.messages.reply_InReplyToView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83fe63b22d68a39a7d404ee2f6074c7adcab79f16e11655d08a37ac3065be59a -size 9327 +oid sha256:4d2cd5afa60a792cc025b6eb580556162b06f03e0bd15b1dad8fa5294ce49e17 +size 9315 diff --git a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png index cc6bf7ea12..92f956925d 100644 --- a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7515c7be72796a399518d0229c4c656e8bbaa759f5710782926b393e65a0dfec -size 52291 +oid sha256:40f902114622ce212797bd9c0a7cf8fa9d465cacf1f32af99422558b35907d02 +size 52217 diff --git a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png index 5ffe8738ed..c36a2ce217 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17b1cc7c68f7f692d819b05cf2ed01454b2dfdf33d847dcabdfe3823aa2858d5 -size 58106 +oid sha256:e7326fadd145ca624810e858cc3413fb56a25f875854b7d2e75cd7e1d1b4134f +size 58018 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png index 15e163db41..f9cbe155c1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4c0e08d0814afded19cccab71e3e25f93bafbb23e285a2d9885b55db95513b3 -size 56085 +oid sha256:dbb50973fb9da0bb43708e3e608de3a22a301a2dc0d8c699bd9e0f59d5b44f20 +size 55991 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png index 566e8a69fe..c6d05fe805 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a28623ac9a17d8d707a92b229e106836cbc04de108598f573e8ec8e89e3d997 -size 65096 +oid sha256:987e6f677fe5412dcd5c94a883bf5966b18d048b31f189614ae2763823e319e9 +size 65017 diff --git a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png index cc6bf7ea12..92f956925d 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7515c7be72796a399518d0229c4c656e8bbaa759f5710782926b393e65a0dfec -size 52291 +oid sha256:40f902114622ce212797bd9c0a7cf8fa9d465cacf1f32af99422558b35907d02 +size 52217 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png index d3d070cca3..77f972cdd8 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd776bc6cc47e98bb697fd45118580d809504af449e66f8e6233e239746635e -size 73608 +oid sha256:49de6907472a673c5b79ae0f20a9939f9f6786fc757b9e1ac96077be34557d06 +size 73249 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png index e0068535e0..ac61252e96 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b318e520274fa61235bb3078a8e43f50f00cb3e995691f7c250aafcff34adf6 -size 60221 +oid sha256:29a442ba2414d64a550f4bded4835e11daeff0bd55031689af7072fba0fadcfd +size 59788 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png index c78fd312b6..e9b333dc05 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4265b64af0d85d26fee18c5840c78dc248dca6c0188f7d05599a17533bbf7c56 -size 73032 +oid sha256:e5493a95039dae9babedde3214acbbff01c7b53a3b454251f234c6cdb09bbd35 +size 72797 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png index f3f25e7b89..03a3fc0a40 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0046b0b393a115565f40ef8e3d56e44b2f0e60bac1c58f460c5b0ccc51c0758 -size 81592 +oid sha256:e817846aa9983e8e38ba6c8ff29ec205636e7b9a72b461215c16eb06e22ceb0e +size 81614 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png index 92ee2953f2..bef0dfd03d 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a67d6255f9b0f17dfc39655a9006d616e5a000edde5c43675f9c05edbd61eb8e -size 62741 +oid sha256:ddf5dbaf0ad4247513d38a34e89897d099daa8e3f6c0bf9495ac65dad0771f34 +size 62532 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png index 3d9ecdc028..9267d1cb6c 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bafc0bc7aa73c3d984e01b4d94a1f2a50a2457add0b202b89bec0acd5f8a66f -size 61646 +oid sha256:35b64678b72a2e2433d278f717d859dc6f53414db565ed325604e0911e8a7e4b +size 61375 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png index 34d58521be..a43cafd104 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1afc95b901ad97456215f1a2f8fde7fe35f5837032b906eea6053f14814d4218 -size 67238 +oid sha256:59b8beb90fbe005555a4de1d67230362623ab9b715796abb3a223f028bf89d5a +size 66981 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png index 7696ab1dd2..1f186b58be 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:455dc658af36e11e5175585c6965731b4493428596fb5ef0b77b6b89abd007e4 -size 90662 +oid sha256:0ad07ef8db1754dadbca19207273bbbffc83a591b615c22a6d38fcb8322af072 +size 90155 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png index 409f5c23b0..558f92c29b 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4514e0bd54db4822486ed0d762768f186cfb24611fd6a8161567b17bd20462a -size 61057 +oid sha256:e130d13f832caaa3b4016c2a915610bde46f0d34e7b394d52ccfb96f367afa74 +size 60683 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png index c20f81e2a3..0db525b4a1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6408e9a851a3976291270c08a6cc3ec30a8b1148f2034b6fa7c5bc8a66111b40 -size 62277 +oid sha256:72db00abddd84b586596e1dc61fe89b27096fbc08ba82d23eaf703c5c83173fd +size 60746 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png index a874dc1806..b14578a682 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8911a8ec35fb8ff3ecb5fc3eec7503ed53d5643c19c2726044ae02569aa374fa -size 69866 +oid sha256:926284ff1d11c483707b568100fbdc8382a1a61ba146a0ef23594089bb3038c8 +size 69406 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png index e2a8343fef..340cea1e36 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fa428a419c4693a3bf3039aaaf81968ffbb1d330208170efd60f9e02768f20e -size 60619 +oid sha256:2cf0dd833df3ace9700622b31f64513a3c76908d6651dbb0ffcc3b5a6a4dce9e +size 60201 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png index ec8d9ddde3..ef613cf899 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b18c02b0e05fe86aca68378a112e698958780e30891c5c0a3e7f316ffd00f657 -size 72919 +oid sha256:164558c7d8cdd701a1d11e4f3df0448818a47eecb06024904e14c29651800f56 +size 72929 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png index 495d5b4fff..a16f0aaf45 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef6a20ea4f3d4b64b294566b242ea8f6021d1a7f54555d6cdbc90f3ed41bf6df -size 56467 +oid sha256:b801d74b3feee574ba81d9d2e270090c86e3c007daa9b96674c752e6428a5d9e +size 56175 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png index 45025e9fb0..39c6f3f77f 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eceb56163d5b3a4398fc7711b76b2b09cf98be3006c0f2469011b233cb2751a8 -size 71145 +oid sha256:e088310a4666c06e55fa88ef9d209e24ff681f9caaf148b1f1cbf4e0a8954206 +size 71274 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png index 876f0759f5..a906ed4d88 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f88e186b6a2c7bb78a7d0a3244324fb873616f72ab77b7cbe71354671241025 -size 82466 +oid sha256:8ffff1b75987e4b692fbec95fdc6bac243fdfb741e24d56f2e0f3b05c1b9a9a2 +size 82793 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png index a95036ff4a..6ffea11e25 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:669073bf986640f0602b24c8f203f6272eeb83d57c9fb6e88757364b0bdd55a6 -size 59507 +oid sha256:17b44b1796d438eb804fec2aa1935c9ef0fbac16b90c55ee57ae0393862391c8 +size 59342 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png index 592cf9b318..0bcdff0b59 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:586b85f5d172f37e9fc0f90317b31826844a6ee598cc1835df72d398e1c57caf -size 58625 +oid sha256:467e422cef79f2be11df55814794472113b434822434301e52bdfd12c094deae +size 58453 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png index 256f3f11be..d28b54f3cc 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f11d0c70af70619812455a42dcf01c9f891f727868c276975003aeb3bf8f97e -size 66007 +oid sha256:e15f573b4d081b17f91ad2e19cc4920063dad46755217145fb8b13288eb2ad3b +size 65754 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png index 0fb81544f4..1e87d303a9 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:503b031ed7f21cb022b5e1d41ed83baa8998469e5140008f349bd755b2e76416 -size 101507 +oid sha256:d80a1a50b289631c432e1247af2ec1a41a9cb233f4ef9c5f6c964784113ff401 +size 101387 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png index 8193b57db1..7903b1ba42 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26bc4f2e52ed22162d4c8a720f4d0b2793b3d97e45d3463616b3b21b0c4638af -size 57853 +oid sha256:157c70292051f5dd737f309b2bdadddbe6cbf7ec3b322a3c47b937edc4df72e0 +size 57461 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png index 92082e6e30..a997076a87 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cca5604149f542ec66a1be21a9ee8220405c49e40e7f974d83d6c60d7133e4bb -size 59690 +oid sha256:44df9e6774110798c72042f560bcc344a5b292f11cba5893190ad78513a8d07e +size 57468 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png index 030c42869a..b198b87ff5 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fcd11760900caeb73940f5618b5c33b54827e9e4a25e2f95ebc1e0200b7cd49 -size 68033 +oid sha256:8323ee0e5869c5408cc17476fb603af972fa570d1e6896bc063ce401f4d1582f +size 67617 diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png index 29186b70be..5515d78ee1 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3be967e3c38366b75f13003171dce8d03886814d9b8663768d28876672f041e5 -size 57219 +oid sha256:31e974364b144d6270b890bf7293ca84a8680105d3914d5d6d2c1fa766b463a1 +size 56853 diff --git a/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png index 5b616e7a55..98ecbe8286 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6964f7b2f51a7d928d49038d1ed48b4576163c329564e4bd928c32cb9ed6648 -size 55506 +oid sha256:65a53d64c78f99ff6d36f8e6597ba7404a7e27581d823a5674d46459d15a97ca +size 55407 diff --git a/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png index 5fce0aa30e..fe5975ff4f 100644 --- a/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png +++ b/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7636fa5e1fb1ef1e5972f7c973ca0282c87e6624fb150abd7f9198083c39fc29 -size 43642 +oid sha256:ea1736c6617804eb12450189db18dec3d1959b2d426aeb298c6164609f23319c +size 43565 diff --git a/screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png b/screenshots/de/services.apperror.api_AppErrorView_Day_0_de.png similarity index 100% rename from screenshots/de/services.apperror.impl_AppErrorView_Day_0_de.png rename to screenshots/de/services.apperror.api_AppErrorView_Day_0_de.png diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 62bb50771b..df612e28d9 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,87 +1,87 @@ // Generated file, do not edit export const screenshots = [ ["en","en-dark","de",], -["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20532,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20553,], ["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_0_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_0_en",0,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20532,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20532,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20532,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20532,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20532,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20532,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20532,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20532,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20532,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20532,], -["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20532,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20553,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20553,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20553,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20553,], ["features.login.impl.accountprovider_AccountProviderView_Day_0_en","features.login.impl.accountprovider_AccountProviderView_Night_0_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_1_en","features.login.impl.accountprovider_AccountProviderView_Night_1_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_2_en","features.login.impl.accountprovider_AccountProviderView_Night_2_en",0,], ["features.login.impl.accountprovider_AccountProviderView_Day_3_en","features.login.impl.accountprovider_AccountProviderView_Night_3_en",0,], -["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20532,], -["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20532,], +["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20553,], +["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20553,], ["features.messages.impl.actionlist_ActionListViewContent_Day_0_en","features.messages.impl.actionlist_ActionListViewContent_Night_0_en",0,], -["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20532,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20553,], ["features.messages.impl.actionlist_ActionListViewContent_Day_1_en","features.messages.impl.actionlist_ActionListViewContent_Night_1_en",0,], -["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20532,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20532,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20532,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20532,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20532,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20532,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20532,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20532,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20532,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20532,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20553,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20553,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20553,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20553,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20553,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20553,], ["libraries.designsystem.theme.components_AllIcons_Icons_en","",0,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20532,], -["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20532,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20532,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20532,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20532,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20553,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20553,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20553,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20553,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20553,], ["libraries.designsystem.components_Announcement_Day_0_en","libraries.designsystem.components_Announcement_Night_0_en",0,], -["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20532,], +["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20556,], ["libraries.designsystem.components.async_AsyncActionView_Day_0_en","libraries.designsystem.components.async_AsyncActionView_Night_0_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20532,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20553,], ["libraries.designsystem.components.async_AsyncActionView_Day_2_en","libraries.designsystem.components.async_AsyncActionView_Night_2_en",0,], -["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20532,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20553,], ["libraries.designsystem.components.async_AsyncActionView_Day_4_en","libraries.designsystem.components.async_AsyncActionView_Night_4_en",0,], -["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20532,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20553,], ["libraries.designsystem.components.async_AsyncIndicatorFailure_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorFailure_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncIndicatorLoading_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorLoading_Night_0_en",0,], ["libraries.designsystem.components.async_AsyncLoading_Day_0_en","libraries.designsystem.components.async_AsyncLoading_Night_0_en",0,], -["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20532,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20553,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_0_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_0_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_2_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_2_en",0,], @@ -91,19 +91,19 @@ export const screenshots = [ ["libraries.matrix.ui.components_AttachmentThumbnail_Day_6_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_6_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_7_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_7_en",0,], ["libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en",0,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20532,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20532,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20553,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20553,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en",0,], ["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en",0,], -["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20532,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en","",0,], ["libraries.matrix.ui.components_AvatarPickerSizes_Day_0_en","libraries.matrix.ui.components_AvatarPickerSizes_Night_0_en",0,], ["libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en","libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en",0,], @@ -133,22 +133,22 @@ export const screenshots = [ ["libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradientDisabled_Night_0_en",0,], ["libraries.designsystem.modifiers_BackgroundVerticalGradient_Day_0_en","libraries.designsystem.modifiers_BackgroundVerticalGradient_Night_0_en",0,], ["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,], -["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20532,], +["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20553,], ["libraries.designsystem.atomic.atoms_BetaLabel_Day_0_en","libraries.designsystem.atomic.atoms_BetaLabel_Night_0_en",0,], ["libraries.designsystem.components_BigIcon_Day_0_en","libraries.designsystem.components_BigIcon_Night_0_en",0,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20532,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20532,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20532,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20532,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20532,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20532,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20532,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20553,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20553,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20532,], -["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20532,], -["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20532,], -["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20532,], -["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20532,], +["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20553,], +["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20553,], ["features.rageshake.impl.bugreport_BugReportViewNight_0_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_1_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_2_en","",0,], @@ -159,141 +159,141 @@ export const screenshots = [ ["features.messages.impl.timeline.components_CallMenuItem_Day_0_en","features.messages.impl.timeline.components_CallMenuItem_Night_0_en",0,], ["features.messages.impl.timeline.components_CallMenuItem_Day_1_en","features.messages.impl.timeline.components_CallMenuItem_Night_1_en",0,], ["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20532,], -["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20532,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20553,], +["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20553,], ["features.messages.impl.timeline.components_CallMenuItem_Day_5_en","features.messages.impl.timeline.components_CallMenuItem_Night_5_en",0,], -["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20532,], +["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20553,], ["features.messages.impl.timeline.components_CallMenuItem_Day_7_en","features.messages.impl.timeline.components_CallMenuItem_Night_7_en",0,], ["features.call.impl.ui_CallScreenView_Day_0_en","features.call.impl.ui_CallScreenView_Night_0_en",0,], -["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20532,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20532,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20532,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20532,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20532,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20532,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20553,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20553,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20553,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20553,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20553,], ["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en",0,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20532,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20532,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20532,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20553,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20553,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20553,], ["features.login.impl.changeserver_ChangeServerView_Day_0_en","features.login.impl.changeserver_ChangeServerView_Night_0_en",0,], -["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20532,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20532,], -["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20532,], -["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20532,], -["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20532,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20553,], +["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20553,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20532,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20553,], ["libraries.designsystem.theme.components_Checkboxes_Toggles_en","",0,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20532,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20532,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20532,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20532,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20532,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20532,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20532,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20532,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20553,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20553,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20553,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], ["libraries.designsystem.theme_ColorAliases_Day_0_en","libraries.designsystem.theme_ColorAliases_Night_0_en",0,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20532,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20532,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20532,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20532,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20532,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20532,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20532,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20553,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20553,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20553,], ["libraries.textcomposer_ComposerModeView_Day_1_en","libraries.textcomposer_ComposerModeView_Night_1_en",0,], ["libraries.textcomposer_ComposerModeView_Day_2_en","libraries.textcomposer_ComposerModeView_Night_2_en",0,], ["libraries.textcomposer_ComposerModeView_Day_3_en","libraries.textcomposer_ComposerModeView_Night_3_en",0,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20532,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20532,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20532,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20532,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20532,], -["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20532,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20553,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20553,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20553,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20553,], ["libraries.designsystem.components.dialogs_ConfirmationDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ConfirmationDialog_Day_0_en","libraries.designsystem.components.dialogs_ConfirmationDialog_Night_0_en",0,], ["features.networkmonitor.api.ui_ConnectivityIndicator_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicator_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_CounterAtom_Day_0_en","libraries.designsystem.atomic.atoms_CounterAtom_Night_0_en",0,], -["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20532,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20532,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20532,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20532,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20532,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20532,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20532,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20532,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20532,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20532,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20532,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20532,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20532,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20532,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20532,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20532,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20532,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20532,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20532,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20532,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20553,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20553,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20553,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20553,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20553,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20553,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20553,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20553,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20553,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20553,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20553,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20553,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20553,], ["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en",0,], -["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20532,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20532,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20532,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20532,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20532,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20532,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20532,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20553,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20553,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_0_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_0_en",0,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20532,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20532,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20532,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20553,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20553,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20553,], ["features.logout.impl.direct_DefaultDirectLogoutView_Day_4_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_4_en",0,], -["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20532,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20553,], ["features.licenses.impl.details_DependenciesDetailsView_Day_0_en","features.licenses.impl.details_DependenciesDetailsView_Night_0_en",0,], -["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20532,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20532,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20532,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20532,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20532,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20532,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20532,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20532,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20532,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_3_en","features.preferences.impl.developer_DeveloperSettingsView_Night_3_en",20532,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20553,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20553,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_3_en","features.preferences.impl.developer_DeveloperSettingsView_Night_3_en",20553,], ["libraries.designsystem.theme.components_DialogWithDestructiveButton_Dialog_with_destructive_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithOnlyMessageAndOkButton_Dialog_with_only_message_and_ok_button_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithThirdButton_Dialog_with_third_button_Dialogs_en","",0,], @@ -308,19 +308,19 @@ export const screenshots = [ ["libraries.designsystem.text_DpScale_1_0f__en","",0,], ["libraries.designsystem.text_DpScale_1_5f__en","",0,], ["libraries.designsystem.theme.components_DropdownMenuItem_Menus_en","",0,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20532,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20532,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20532,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20532,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20532,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20532,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20532,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20532,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20532,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20532,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20532,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20532,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20532,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20553,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20553,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20553,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20553,], ["libraries.matrix.ui.components_EditableOrgAvatarRtl_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatarRtl_Night_0_en",0,], ["libraries.matrix.ui.components_EditableOrgAvatar_Day_0_en","libraries.matrix.ui.components_EditableOrgAvatar_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_Night_0_en",0,], @@ -328,28 +328,28 @@ export const screenshots = [ ["libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,], ["features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en",0,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20532,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20532,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20553,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20553,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_2_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_2_en",0,], ["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_3_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_3_en",0,], ["libraries.ui.common.nodes_EmptyView_Day_0_en","libraries.ui.common.nodes_EmptyView_Night_0_en",0,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20532,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20532,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20532,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20532,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20532,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20532,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20532,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20532,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20532,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20532,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20553,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20553,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20553,], ["features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en","features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en",0,], ["libraries.designsystem.components_ExpandableBottomSheetLayout_en","",0,], ["libraries.featureflag.ui_FeatureListView_Day_0_en","libraries.featureflag.ui_FeatureListView_Night_0_en",0,], @@ -366,50 +366,50 @@ export const screenshots = [ ["libraries.designsystem.theme.components_FilledTextFieldValueLight_TextFields_en","",0,], ["libraries.designsystem.theme.components_FilledTextFieldValueTextFieldDark_TextFields_en","",0,], ["libraries.designsystem.theme.components_FloatingActionButton_Floating_Action_Buttons_en","",0,], +["features.messages.impl.timeline.components_FloatingDateBadge_Day_0_en","features.messages.impl.timeline.components_FloatingDateBadge_Night_0_en",0,], ["libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en","libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en",0,], ["features.messages.impl.timeline.focus_FocusRequestStateView_Day_0_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_0_en",0,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20532,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20532,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20532,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20553,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20553,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20553,], ["features.messages.impl.timeline.components_FocusedEvent_Day_0_en","features.messages.impl.timeline.components_FocusedEvent_Night_0_en",0,], ["libraries.textcomposer.components_FormattingOption_Day_0_en","libraries.textcomposer.components_FormattingOption_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_0_en","features.forward.impl_ForwardMessagesView_Night_0_en",0,], ["features.forward.impl_ForwardMessagesView_Day_1_en","features.forward.impl_ForwardMessagesView_Night_1_en",0,], ["features.forward.impl_ForwardMessagesView_Day_2_en","features.forward.impl_ForwardMessagesView_Night_2_en",0,], -["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20532,], -["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20532,], +["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20553,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20553,], ["libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en",0,], ["libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en",0,], ["features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en","features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPageScrollable_Night_0_en",0,], ["libraries.designsystem.atomic.pages_HeaderFooterPage_Day_0_en","libraries.designsystem.atomic.pages_HeaderFooterPage_Night_0_en",0,], -["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20532,], -["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20532,], -["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20532,], -["features.home.impl.spaces_HomeSpacesView_Day_3_en","features.home.impl.spaces_HomeSpacesView_Night_3_en",20532,], -["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20532,], -["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20532,], +["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20553,], +["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20553,], +["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20553,], +["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20553,], +["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20553,], ["features.home.impl.components_HomeTopBarSpaces_Day_0_en","features.home.impl.components_HomeTopBarSpaces_Night_0_en",0,], -["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20532,], -["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20532,], +["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20553,], +["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20553,], ["features.home.impl_HomeViewA11y_en","",0,], -["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20532,], -["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20532,], +["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20553,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20553,], ["features.home.impl_HomeView_Day_11_en","features.home.impl_HomeView_Night_11_en",0,], ["features.home.impl_HomeView_Day_12_en","features.home.impl_HomeView_Night_12_en",0,], -["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20532,], -["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20532,], -["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20532,], -["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20532,], -["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20532,], -["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20532,], -["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20532,], -["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20532,], -["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20532,], -["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20532,], -["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20532,], -["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20532,], -["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20532,], +["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20553,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20553,], +["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20553,], +["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20553,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20553,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20553,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20553,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20553,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20553,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20553,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20553,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20553,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20553,], ["libraries.designsystem.theme.components_HorizontalDivider_Dividers_en","",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en",0,], ["libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en",0,], @@ -420,12 +420,12 @@ export const screenshots = [ ["libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en",0,], ["libraries.designsystem.theme.components_IconToggleButton_Toggles_en","",0,], -["appicon.element_Icon_en","",0,], ["appicon.enterprise_Icon_en","",0,], +["appicon.element_Icon_en","",0,], ["libraries.designsystem.icons_IconsOther_Day_0_en","libraries.designsystem.icons_IconsOther_Night_0_en",0,], ["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en",0,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20532,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20532,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20553,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20553,], ["libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_0_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_0_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_10_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_10_en",0,], @@ -433,117 +433,117 @@ export const screenshots = [ ["libraries.matrix.ui.messages.reply_InReplyToView_Day_1_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_1_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_2_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_2_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20532,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20553,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_6_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_6_en",0,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en",0,], -["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20532,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20553,], ["libraries.matrix.ui.messages.reply_InReplyToView_Day_9_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_9_en",0,], -["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20532,], -["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20532,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20553,], +["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20553,], ["features.verifysession.impl.incoming_IncomingVerificationViewA11y_en","",0,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20532,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20532,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20553,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20553,], ["libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en","libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en",0,], ["libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en","libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en",0,], ["libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Day_0_en","libraries.matrix.ui.media_InitialsAvatarBitmapGenerator_Night_0_en",0,], -["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20532,], -["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20532,], -["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20532,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20553,], ["features.invitepeople.impl_InvitePeopleView_Day_2_en","features.invitepeople.impl_InvitePeopleView_Night_2_en",0,], ["features.invitepeople.impl_InvitePeopleView_Day_3_en","features.invitepeople.impl_InvitePeopleView_Night_3_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20532,], -["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20532,], -["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20532,], -["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20532,], +["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20553,], +["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20553,], ["features.invitepeople.impl_InvitePeopleView_Day_8_en","features.invitepeople.impl_InvitePeopleView_Night_8_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20532,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20532,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20532,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20532,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20532,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20532,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20532,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20532,], +["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20553,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20553,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20553,], ["features.joinroom.impl_JoinRoomView_Day_0_en","features.joinroom.impl_JoinRoomView_Night_0_en",0,], -["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20532,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20532,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20532,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20532,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20553,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20553,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20553,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20553,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], -["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20532,], -["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20532,], +["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20553,], +["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20553,], ["features.leaveroom.impl_LeaveRoomView_Day_0_en","features.leaveroom.impl_LeaveRoomView_Night_0_en",0,], -["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20532,], -["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20532,], -["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20532,], -["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20532,], -["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20532,], -["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20532,], -["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20532,], -["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20532,], +["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20553,], +["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20553,], +["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20553,], ["libraries.designsystem.background_LightGradientBackground_Day_0_en","libraries.designsystem.background_LightGradientBackground_Night_0_en",0,], ["libraries.designsystem.theme.components_LinearProgressIndicator_Progress_Indicators_en","",0,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20532,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20532,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20532,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20532,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20532,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20532,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20553,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20553,], ["features.messages.impl.link_LinkView_Day_0_en","features.messages.impl.link_LinkView_Night_0_en",0,], -["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20532,], +["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20553,], ["libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_ListDialog_Day_0_en","libraries.designsystem.components.dialogs_ListDialog_Night_0_en",0,], ["libraries.designsystem.theme.components_ListItemPrimaryActionWithIcon_List_item_-_Primary_action_&_Icon_List_items_en","",0,], @@ -598,41 +598,43 @@ export const screenshots = [ ["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en","",0,], ["libraries.textcomposer.components_LiveWaveformView_Day_0_en","libraries.textcomposer.components_LiveWaveformView_Night_0_en",0,], ["appnav.room.joined_LoadingRoomNodeView_Day_0_en","appnav.room.joined_LoadingRoomNodeView_Night_0_en",0,], -["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20532,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20532,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20532,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20532,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20553,], +["libraries.designsystem.components_LocationPin_Day_0_en","libraries.designsystem.components_LocationPin_Night_0_en",0,], +["features.location.impl.common.ui_LocationShareRow_Day_0_en","features.location.impl.common.ui_LocationShareRow_Night_0_en",0,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20553,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20553,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20553,], ["appnav.loggedin_LoggedInView_Day_0_en","appnav.loggedin_LoggedInView_Night_0_en",0,], -["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20532,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20532,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20532,], -["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20532,], -["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20532,], -["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20532,], -["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20532,], -["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20532,], -["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20532,], -["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20532,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20532,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20532,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20532,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20532,], -["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20532,], -["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20532,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20532,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20532,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20532,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20532,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20532,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20532,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20532,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20532,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20532,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20553,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20553,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20553,], +["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20553,], +["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20553,], +["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20553,], +["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20553,], +["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20553,], +["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20553,], +["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20553,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20553,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20553,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20553,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20553,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20553,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20553,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20553,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20553,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20553,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20553,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20553,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20553,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20553,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20532,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20532,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20532,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20532,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20553,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20553,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20553,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20553,], ["libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en","libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomInfo_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en",0,], @@ -646,22 +648,22 @@ export const screenshots = [ ["libraries.matrix.ui.components_MatrixUserRow_Day_1_en","libraries.matrix.ui.components_MatrixUserRow_Night_1_en",0,], ["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en",0,], -["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20532,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20532,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20553,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20553,], ["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",0,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20532,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20532,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20553,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20553,], ["libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en","libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en",0,], ["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en",0,], @@ -669,14 +671,14 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en","libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_0_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_10_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20532,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20532,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20553,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20532,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_1_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20532,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20553,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,], @@ -690,7 +692,7 @@ export const screenshots = [ ["libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en","libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en",0,], ["libraries.designsystem.theme.components.previews_Menu_Menus_en","",0,], ["features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en","features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en",0,], -["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20532,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20553,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_0_en","features.messages.impl.timeline.components_MessageEventBubble_Night_0_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_1_en","features.messages.impl.timeline.components_MessageEventBubble_Night_1_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_2_en","features.messages.impl.timeline.components_MessageEventBubble_Night_2_en",0,], @@ -699,7 +701,7 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessageEventBubble_Day_5_en","features.messages.impl.timeline.components_MessageEventBubble_Night_5_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_6_en","features.messages.impl.timeline.components_MessageEventBubble_Night_6_en",0,], ["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,], -["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20532,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20553,], ["features.messages.impl.timeline.components_MessageStateEventContainer_Day_0_en","features.messages.impl.timeline.components_MessageStateEventContainer_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButtonExtra_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonExtra_Night_0_en",0,], @@ -708,23 +710,23 @@ export const screenshots = [ ["features.messages.impl.timeline.components_MessagesReactionButton_Day_2_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_2_en",0,], ["features.messages.impl.timeline.components_MessagesReactionButton_Day_3_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_3_en",0,], ["features.messages.impl_MessagesViewA11y_en","",0,], -["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20532,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20532,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20532,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20532,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20532,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20532,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20532,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20532,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20532,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20532,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20532,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20532,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20532,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20532,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20532,], +["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20553,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20553,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20553,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20553,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20553,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20553,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20553,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20553,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20553,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20553,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20553,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20553,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20553,], ["features.migration.impl_MigrationView_Day_0_en","features.migration.impl_MigrationView_Night_0_en",0,], -["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20532,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20553,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], ["appicon.element_MonochromeIcon_en","",0,], @@ -735,113 +737,112 @@ export const screenshots = [ ["libraries.designsystem.components.list_MutipleSelectionListItemSelected_Multiple_selection_List_item_-_selection_in_supporting_text_List_items_en","",0,], ["libraries.designsystem.components.list_MutipleSelectionListItem_Multiple_selection_List_item_-_no_selection_List_items_en","",0,], ["libraries.designsystem.theme.components_NavigationBar_App_Bars_en","",0,], -["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20532,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20532,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20532,], +["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20553,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20553,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20553,], ["features.linknewdevice.impl.screens.number.component_NumberTextField_Day_0_en","features.linknewdevice.impl.screens.number.component_NumberTextField_Night_0_en",0,], ["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20532,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20532,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20553,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20553,], ["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,], -["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20532,], +["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20553,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_12_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_12_en",0,], ["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_13_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_13_en",0,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20532,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20532,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20553,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20553,], ["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_OutlinedButtonSmall_Buttons_en","",0,], -["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20532,], -["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20532,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20532,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20532,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20532,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20532,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20553,], +["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20553,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20553,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20553,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20553,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20553,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_Night_0_en",0,], -["libraries.designsystem.components_PinIcon_Day_0_en","libraries.designsystem.components_PinIcon_Night_0_en",0,], ["features.lockscreen.impl.unlock.keypad_PinKeypad_Day_0_en","features.lockscreen.impl.unlock.keypad_PinKeypad_Night_0_en",0,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20532,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20532,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20553,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20553,], ["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en",0,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20532,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20532,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20532,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20532,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20532,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20532,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20553,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20553,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20553,], ["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,], ["libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Day_0_en","libraries.designsystem.atomic.atoms_PlaybackSpeedButton_Night_0_en",0,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20532,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20532,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20532,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20532,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20532,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20553,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20553,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en",0,], ["features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en",0,], -["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20532,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20532,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20532,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20532,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20532,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20532,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20532,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20532,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20532,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20532,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20532,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20553,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20553,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20553,], ["features.poll.api.pollcontent_PollTitleView_Day_0_en","features.poll.api.pollcontent_PollTitleView_Night_0_en",0,], ["libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en","",0,], @@ -855,215 +856,215 @@ export const screenshots = [ ["libraries.designsystem.components.preferences_PreferenceRow_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSlide_Preferences_en","",0,], ["libraries.designsystem.components.preferences_PreferenceSwitch_Preferences_en","",0,], -["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20532,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20532,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20532,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20532,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20553,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20553,], ["features.messages.impl.timeline.components.event_ProgressButton_Day_0_en","features.messages.impl.timeline.components.event_ProgressButton_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20532,], -["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20532,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20553,], ["libraries.designsystem.components_ProgressDialogWithTextAndContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithTextAndContent_Night_0_en",0,], -["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20532,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20532,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20532,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20532,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20532,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20532,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20532,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20532,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20532,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20532,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20532,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20532,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20532,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20532,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20532,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20532,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20532,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20532,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20532,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20532,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20532,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20553,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20553,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20553,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20553,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20553,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20553,], ["libraries.qrcode_QrCodeView_en","",0,], ["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,], -["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20532,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20532,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20553,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20553,], ["features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en",0,], ["features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Day_0_en","features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Night_0_en",0,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20532,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20532,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20532,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20532,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20532,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20532,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20532,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20553,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20553,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20553,], ["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,], ["features.messages.impl.timeline.components_ReplySwipeIndicator_Day_0_en","features.messages.impl.timeline.components_ReplySwipeIndicator_Night_0_en",0,], -["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20532,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20532,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20532,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20532,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20532,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20532,], -["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20532,], -["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20532,], -["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20532,], -["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20532,], -["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20532,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20532,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20532,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20532,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20532,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20532,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20532,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20553,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20553,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20553,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20553,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20553,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20553,], ["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_0_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_0_en",0,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20532,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20532,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20532,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20532,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20532,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20553,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20553,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20553,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20553,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20553,], ["libraries.matrix.ui.room.address_RoomAddressField_Day_0_en","libraries.matrix.ui.room.address_RoomAddressField_Night_0_en",0,], ["features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en",0,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20532,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20532,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20553,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20553,], ["features.roomdetails.impl_RoomDetailsA11y_en","",0,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_16_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_17_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_18_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_19_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_20_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_21_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_22_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20532,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20532,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20532,], -["features.roomdetails.impl_RoomDetails_0_en","",20532,], -["features.roomdetails.impl_RoomDetails_10_en","",20532,], -["features.roomdetails.impl_RoomDetails_11_en","",20532,], -["features.roomdetails.impl_RoomDetails_12_en","",20532,], -["features.roomdetails.impl_RoomDetails_13_en","",20532,], -["features.roomdetails.impl_RoomDetails_14_en","",20532,], -["features.roomdetails.impl_RoomDetails_15_en","",20532,], -["features.roomdetails.impl_RoomDetails_16_en","",20532,], -["features.roomdetails.impl_RoomDetails_17_en","",20532,], -["features.roomdetails.impl_RoomDetails_18_en","",20532,], -["features.roomdetails.impl_RoomDetails_19_en","",20532,], -["features.roomdetails.impl_RoomDetails_1_en","",20532,], -["features.roomdetails.impl_RoomDetails_20_en","",20532,], -["features.roomdetails.impl_RoomDetails_21_en","",20532,], -["features.roomdetails.impl_RoomDetails_22_en","",20532,], -["features.roomdetails.impl_RoomDetails_2_en","",20532,], -["features.roomdetails.impl_RoomDetails_3_en","",20532,], -["features.roomdetails.impl_RoomDetails_4_en","",20532,], -["features.roomdetails.impl_RoomDetails_5_en","",20532,], -["features.roomdetails.impl_RoomDetails_6_en","",20532,], -["features.roomdetails.impl_RoomDetails_7_en","",20532,], -["features.roomdetails.impl_RoomDetails_8_en","",20532,], -["features.roomdetails.impl_RoomDetails_9_en","",20532,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20532,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20532,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20532,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20532,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20532,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20532,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20532,], -["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20532,], -["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20532,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_20_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_21_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_22_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20553,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20553,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20553,], +["features.roomdetails.impl_RoomDetails_0_en","",20553,], +["features.roomdetails.impl_RoomDetails_10_en","",20553,], +["features.roomdetails.impl_RoomDetails_11_en","",20553,], +["features.roomdetails.impl_RoomDetails_12_en","",20553,], +["features.roomdetails.impl_RoomDetails_13_en","",20553,], +["features.roomdetails.impl_RoomDetails_14_en","",20553,], +["features.roomdetails.impl_RoomDetails_15_en","",20553,], +["features.roomdetails.impl_RoomDetails_16_en","",20553,], +["features.roomdetails.impl_RoomDetails_17_en","",20553,], +["features.roomdetails.impl_RoomDetails_18_en","",20553,], +["features.roomdetails.impl_RoomDetails_19_en","",20553,], +["features.roomdetails.impl_RoomDetails_1_en","",20553,], +["features.roomdetails.impl_RoomDetails_20_en","",20553,], +["features.roomdetails.impl_RoomDetails_21_en","",20553,], +["features.roomdetails.impl_RoomDetails_22_en","",20553,], +["features.roomdetails.impl_RoomDetails_2_en","",20553,], +["features.roomdetails.impl_RoomDetails_3_en","",20553,], +["features.roomdetails.impl_RoomDetails_4_en","",20553,], +["features.roomdetails.impl_RoomDetails_5_en","",20553,], +["features.roomdetails.impl_RoomDetails_6_en","",20553,], +["features.roomdetails.impl_RoomDetails_7_en","",20553,], +["features.roomdetails.impl_RoomDetails_8_en","",20553,], +["features.roomdetails.impl_RoomDetails_9_en","",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20553,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20553,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20553,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20553,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20553,], ["features.home.impl.components_RoomListContentView_Day_2_en","features.home.impl.components_RoomListContentView_Night_2_en",0,], -["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20532,], -["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20532,], -["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20532,], -["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20532,], -["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20532,], -["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20532,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20532,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20532,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20532,], +["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20553,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20553,], +["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20553,], +["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20553,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20553,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20553,], +["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20553,], ["features.home.impl.search_RoomListSearchContent_Day_0_en","features.home.impl.search_RoomListSearchContent_Night_0_en",0,], -["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20532,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20532,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20532,], +["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20553,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20553,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20553,], ["features.roommembermoderation.impl_RoomMemberModerationView_Day_9_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_9_en",0,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20532,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20532,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20553,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20553,], ["libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoomPreviewAliasAtom_Night_0_en",0,], -["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20532,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20532,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20532,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20532,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20532,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20532,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20553,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20553,], ["features.home.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.home.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_0_en","features.home.impl.components_RoomSummaryRow_Night_0_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_10_en","features.home.impl.components_RoomSummaryRow_Night_10_en",0,], @@ -1086,16 +1087,16 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_26_en","features.home.impl.components_RoomSummaryRow_Night_26_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_27_en","features.home.impl.components_RoomSummaryRow_Night_27_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_28_en","features.home.impl.components_RoomSummaryRow_Night_28_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20532,], -["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20532,], +["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20553,], +["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20553,], ["features.home.impl.components_RoomSummaryRow_Day_36_en","features.home.impl.components_RoomSummaryRow_Night_36_en",0,], -["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20532,], +["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20553,], ["features.home.impl.components_RoomSummaryRow_Day_3_en","features.home.impl.components_RoomSummaryRow_Night_3_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_4_en","features.home.impl.components_RoomSummaryRow_Night_4_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_5_en","features.home.impl.components_RoomSummaryRow_Night_5_en",0,], @@ -1103,118 +1104,118 @@ export const screenshots = [ ["features.home.impl.components_RoomSummaryRow_Day_7_en","features.home.impl.components_RoomSummaryRow_Night_7_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_8_en","features.home.impl.components_RoomSummaryRow_Night_8_en",0,], ["features.home.impl.components_RoomSummaryRow_Day_9_en","features.home.impl.components_RoomSummaryRow_Night_9_en",0,], -["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20532,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20532,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20532,], -["appicon.enterprise_RoundIcon_en","",0,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20553,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20553,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20553,], ["appicon.element_RoundIcon_en","",0,], +["appicon.enterprise_RoundIcon_en","",0,], ["libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en",0,], -["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20532,], -["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20532,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20532,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20532,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20532,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20532,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20532,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20532,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20553,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20553,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20553,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20553,], ["libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en","",0,], -["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20532,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20553,], ["libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchBarInactive_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsDark_Search_views_en","",0,], ["libraries.designsystem.theme.components_SearchFieldsLight_Search_views_en","",0,], -["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20532,], -["features.startchat.impl.components_SearchSingleUserResultItem_en","",20532,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20532,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20532,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20532,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20532,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20532,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20532,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20532,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20532,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20532,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20532,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20532,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20532,], -["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20532,], +["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20553,], +["features.startchat.impl.components_SearchSingleUserResultItem_en","",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20553,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20553,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20553,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20553,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20553,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20553,], +["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20553,], ["libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_SelectedIndicatorAtom_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_0_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_0_en",0,], ["libraries.matrix.ui.components_SelectedRoomRtl_Day_1_en","libraries.matrix.ui.components_SelectedRoomRtl_Night_1_en",0,], @@ -1228,11 +1229,6 @@ export const screenshots = [ ["libraries.matrix.ui.components_SelectedUser_Day_1_en","libraries.matrix.ui.components_SelectedUser_Night_1_en",0,], ["libraries.matrix.ui.components_SelectedUsersRowList_Day_0_en","libraries.matrix.ui.components_SelectedUsersRowList_Night_0_en",0,], ["libraries.textcomposer.components_SendButtonIcon_Day_0_en","libraries.textcomposer.components_SendButtonIcon_Night_0_en",0,], -["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",20532,], -["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20532,], -["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20532,], -["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20532,], -["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20532,], ["libraries.matrix.ui.messages.sender_SenderName_Day_0_en","libraries.matrix.ui.messages.sender_SenderName_Night_0_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_1_en","libraries.matrix.ui.messages.sender_SenderName_Night_1_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_2_en","libraries.matrix.ui.messages.sender_SenderName_Night_2_en",0,], @@ -1242,28 +1238,33 @@ export const screenshots = [ ["libraries.matrix.ui.messages.sender_SenderName_Day_6_en","libraries.matrix.ui.messages.sender_SenderName_Night_6_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_7_en","libraries.matrix.ui.messages.sender_SenderName_Night_7_en",0,], ["libraries.matrix.ui.messages.sender_SenderName_Day_8_en","libraries.matrix.ui.messages.sender_SenderName_Night_8_en",0,], -["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20532,], -["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20532,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20532,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20532,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20532,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20532,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20532,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20532,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20553,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20553,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20553,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20553,], +["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20553,], +["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20553,], +["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20553,], +["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20553,], +["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20553,], +["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20553,], +["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20553,], ["features.share.impl_ShareView_Day_0_en","features.share.impl_ShareView_Night_0_en",0,], ["features.share.impl_ShareView_Day_1_en","features.share.impl_ShareView_Night_1_en",0,], ["features.share.impl_ShareView_Day_2_en","features.share.impl_ShareView_Night_2_en",0,], -["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20532,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20532,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20532,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20532,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20532,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20532,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20532,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20532,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20532,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20532,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20532,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20553,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20553,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20553,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20553,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20553,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20553,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20553,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20553,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20553,], ["libraries.designsystem.components_SimpleModalBottomSheet_Day_0_en","libraries.designsystem.components_SimpleModalBottomSheet_Night_0_en",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialogContent_Dialogs_en","",0,], ["libraries.designsystem.components.dialogs_SingleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_SingleSelectionDialog_Night_0_en",0,], @@ -1273,107 +1274,107 @@ export const screenshots = [ ["libraries.designsystem.components.list_SingleSelectionListItemUnselectedWithSupportingText_Single_selection_List_item_-_no_selection,_supporting_text_List_items_en","",0,], ["libraries.designsystem.components.list_SingleSelectionListItem_Single_selection_List_item_-_no_selection_List_items_en","",0,], ["libraries.designsystem.theme.components_Sliders_Sliders_en","",0,], -["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20532,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20553,], ["libraries.designsystem.theme.components_SnackbarWithActionAndCloseButton_Snackbar_with_action_and_close_button_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLineAndCloseButton_Snackbar_with_action_and_close_button_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithActionOnNewLine_Snackbar_with_action_on_new_line_Snackbars_en","",0,], ["libraries.designsystem.theme.components_SnackbarWithAction_Snackbar_with_action_Snackbars_en","",0,], ["libraries.designsystem.theme.components_Snackbar_Snackbar_Snackbars_en","",0,], -["features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en","features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en",20532,], +["features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en","features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_SpaceAvatar_Avatars_en","",0,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20532,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20532,], -["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20532,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20553,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20553,], +["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20553,], ["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",0,], -["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20532,], +["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20553,], ["libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Day_0_en","libraries.matrix.ui.components_SpaceMembersViewNoHeroes_Night_0_en",0,], ["libraries.matrix.ui.components_SpaceMembersView_Day_0_en","libraries.matrix.ui.components_SpaceMembersView_Night_0_en",0,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20532,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20532,], -["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20532,], -["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20532,], -["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20532,], -["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20532,], -["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20532,], -["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20532,], -["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20532,], -["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20532,], -["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20532,], -["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20532,], -["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20532,], -["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20532,], -["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20532,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20553,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20553,], +["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20553,], +["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20553,], +["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20553,], +["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20553,], +["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20553,], +["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20553,], +["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20553,], +["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20553,], +["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20553,], +["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20553,], ["libraries.designsystem.modifiers_SquareSizeModifierInsideSquare_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,], ["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,], -["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20532,], -["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20532,], -["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20532,], -["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20532,], -["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20532,], -["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20532,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20532,], +["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20553,], +["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20553,], +["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20553,], +["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20553,], +["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20553,], +["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20553,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20553,], ["features.location.api_StaticMapView_Day_0_en","features.location.api_StaticMapView_Night_0_en",0,], -["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20532,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20553,], ["libraries.designsystem.atomic.pages_SunsetPage_Day_0_en","libraries.designsystem.atomic.pages_SunsetPage_Night_0_en",0,], ["libraries.designsystem.components.button_SuperButton_Day_0_en","libraries.designsystem.components.button_SuperButton_Night_0_en",0,], ["libraries.designsystem.theme.components_Surface_en","",0,], ["libraries.designsystem.theme.components_Switch_Toggles_en","",0,], -["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20532,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20553,], ["libraries.designsystem.components.avatar.internal_TextAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TextButtonLargeLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonLarge_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMediumLowPadding_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonMedium_Buttons_en","",0,], ["libraries.designsystem.theme.components_TextButtonSmall_Buttons_en","",0,], -["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20532,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20532,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20532,], -["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20532,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20532,], -["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20532,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20532,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20532,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20532,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20532,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20532,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20532,], -["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20532,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20532,], -["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20532,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20553,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20553,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20553,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20553,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20553,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20553,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20553,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20553,], ["libraries.textcomposer_TextComposerVoice_Day_0_en","libraries.textcomposer_TextComposerVoice_Night_0_en",0,], ["libraries.designsystem.theme.components_TextDark_Text_en","",0,], -["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20532,], -["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20532,], +["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20553,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20553,], ["libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItemTextFieldValue_Text_field_List_item_-_textfieldvalue_List_items_en","",0,], ["libraries.designsystem.components.list_TextFieldListItem_Text_field_List_item_-_text_List_items_en","",0,], @@ -1385,16 +1386,16 @@ export const screenshots = [ ["libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_3_en","libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_3_en",0,], ["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,], ["libraries.designsystem.theme.components_TextLight_Text_en","",0,], -["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20532,], -["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20532,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20532,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20532,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20532,], +["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20553,], +["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20553,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20553,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20553,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_0_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_1_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20532,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20532,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_7_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_7_en",0,], @@ -1404,18 +1405,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20532,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20553,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_0_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_1_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20532,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en","",0,], @@ -1423,18 +1424,18 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20532,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20532,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20532,], -["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20532,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20532,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20532,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20532,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",0,], @@ -1443,41 +1444,42 @@ export const screenshots = [ ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20532,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",0,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20532,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20532,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20532,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20553,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en",0,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20532,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20532,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20532,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20532,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20532,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",0,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20553,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en",0,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20532,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20532,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20553,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en",0,], -["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20532,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20553,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en",0,], @@ -1486,8 +1488,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_5_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_5_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_6_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_6_en",0,], ["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_7_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_7_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20532,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20532,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20553,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20553,], ["features.messages.impl.timeline.components_TimelineItemStateEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemStateEventRow_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStateView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStateView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en",0,], @@ -1502,8 +1504,8 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_3_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_4_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_5_en",0,], -["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20532,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20532,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20553,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20553,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en",0,], ["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en",0,], @@ -1526,85 +1528,85 @@ export const screenshots = [ ["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_9_en",0,], ["features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Night_0_en",0,], ["features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en",0,], -["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",0,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20532,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20532,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20553,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",0,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_2_en","features.messages.impl.timeline_TimelineView_Night_2_en",0,], ["features.messages.impl.timeline_TimelineView_Day_3_en","features.messages.impl.timeline_TimelineView_Night_3_en",0,], -["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20532,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_5_en","features.messages.impl.timeline_TimelineView_Night_5_en",0,], -["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20532,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20553,], ["features.messages.impl.timeline_TimelineView_Day_7_en","features.messages.impl.timeline_TimelineView_Night_7_en",0,], ["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",0,], ["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,], ["libraries.designsystem.components.avatar.internal_TombstonedRoomAvatar_Avatars_en","",0,], ["libraries.designsystem.theme.components_TopAppBarStr_App_Bars_en","",0,], ["libraries.designsystem.theme.components_TopAppBar_App_Bars_en","",0,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20532,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20532,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20553,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20553,], ["features.messages.impl.typing_TypingNotificationView_Day_0_en","features.messages.impl.typing_TypingNotificationView_Night_0_en",0,], -["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20532,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20532,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20532,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20532,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20532,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20532,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20553,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20553,], ["features.messages.impl.typing_TypingNotificationView_Day_7_en","features.messages.impl.typing_TypingNotificationView_Night_7_en",0,], ["features.messages.impl.typing_TypingNotificationView_Day_8_en","features.messages.impl.typing_TypingNotificationView_Night_8_en",0,], ["libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en",0,], -["libraries.matrix.ui.components_UnresolvedUserRow_en","",20532,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20553,], ["libraries.designsystem.components.avatar.internal_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar.internal_UserAvatarColors_Night_0_en",0,], -["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20532,], -["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20532,], -["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20532,], -["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20532,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20553,], +["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20553,], +["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20553,], +["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20553,], ["features.startchat.impl.components_UserListView_Day_3_en","features.startchat.impl.components_UserListView_Night_3_en",0,], ["features.startchat.impl.components_UserListView_Day_4_en","features.startchat.impl.components_UserListView_Night_4_en",0,], ["features.startchat.impl.components_UserListView_Day_5_en","features.startchat.impl.components_UserListView_Night_5_en",0,], ["features.startchat.impl.components_UserListView_Day_6_en","features.startchat.impl.components_UserListView_Night_6_en",0,], -["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20532,], +["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20553,], ["features.startchat.impl.components_UserListView_Day_8_en","features.startchat.impl.components_UserListView_Night_8_en",0,], -["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20532,], +["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20553,], ["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,], ["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,], ["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,], -["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20532,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20532,], -["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20532,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20532,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20532,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20532,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20532,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20532,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20532,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20532,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20532,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20532,], -["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20532,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20553,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20553,], +["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20553,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20553,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20553,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20553,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20553,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20553,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20553,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20553,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20553,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20553,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20553,], ["features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en","features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en",0,], ["libraries.designsystem.ruler_VerticalRuler_Day_0_en","libraries.designsystem.ruler_VerticalRuler_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en",0,], ["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en",0,], -["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20532,], -["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20532,], +["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20553,], +["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20553,], ["features.viewfolder.impl.file_ViewFileView_Day_0_en","features.viewfolder.impl.file_ViewFileView_Night_0_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_1_en","features.viewfolder.impl.file_ViewFileView_Night_1_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_2_en","features.viewfolder.impl.file_ViewFileView_Night_2_en",0,], -["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20532,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20553,], ["features.viewfolder.impl.file_ViewFileView_Day_4_en","features.viewfolder.impl.file_ViewFileView_Night_4_en",0,], ["features.viewfolder.impl.file_ViewFileView_Day_5_en","features.viewfolder.impl.file_ViewFileView_Night_5_en",0,], ["features.viewfolder.impl.folder_ViewFolderView_Day_0_en","features.viewfolder.impl.folder_ViewFolderView_Night_0_en",0,], diff --git a/services/apperror/api/build.gradle.kts b/services/apperror/api/build.gradle.kts index c0893bd7e8..62d3501655 100644 --- a/services/apperror/api/build.gradle.kts +++ b/services/apperror/api/build.gradle.kts @@ -16,4 +16,5 @@ android { dependencies { implementation(libs.coroutines.core) + implementation(projects.libraries.designsystem) } diff --git a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorView.kt similarity index 86% rename from services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt rename to services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorView.kt index 3794234c79..5a2a5110f5 100644 --- a/services/apperror/impl/src/main/kotlin/io/element/android/services/apperror/impl/AppErrorView.kt +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorView.kt @@ -6,14 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.services.apperror.impl +package io.element.android.services.apperror.api import androidx.compose.runtime.Composable import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.services.apperror.api.AppErrorState -import io.element.android.services.apperror.api.aAppErrorState @Composable fun AppErrorView( diff --git a/services/apperror/impl/build.gradle.kts b/services/apperror/impl/build.gradle.kts index b72b54c198..75440dc5a3 100644 --- a/services/apperror/impl/build.gradle.kts +++ b/services/apperror/impl/build.gradle.kts @@ -10,7 +10,7 @@ import extension.testCommonDependencies */ plugins { - id("io.element.android-compose-library") + id("io.element.android-library") } setupDependencyInjection() @@ -22,8 +22,6 @@ android { dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) implementation(libs.coroutines.core) diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeActiveRoomsHolder.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeActiveRoomsHolder.kt new file mode 100644 index 0000000000..7561932fc2 --- /dev/null +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeActiveRoomsHolder.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.services.appnavstate.test + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.services.appnavstate.api.ActiveRoomsHolder + +class FakeActiveRoomsHolder : ActiveRoomsHolder { + private var room: JoinedRoom? = null + + override fun addRoom(room: JoinedRoom) { + this.room = room + } + + override fun getActiveRoom(sessionId: SessionId): JoinedRoom? { + return room + } + + override fun getActiveRoomMatching(sessionId: SessionId, roomId: RoomId): JoinedRoom? { + return null + } + + override fun removeRoom(sessionId: SessionId, roomId: RoomId) { + room = null + } + + override fun clear(sessionId: SessionId) { + } +} diff --git a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt index 0b088b5de0..a611fa83cf 100644 --- a/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/base/ScreenshotTest.kt @@ -23,6 +23,7 @@ import app.cash.paparazzi.Paparazzi import app.cash.paparazzi.RenderExtension import app.cash.paparazzi.TestName import com.android.resources.NightMode +import com.android.resources.ScreenOrientation import io.element.android.compound.theme.ElementTheme import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview @@ -122,6 +123,7 @@ object PaparazziPreviewRule { ): Paparazzi { val densityScale = deviceConfig.density.dpiValue / 160f val customScreenHeight = preview.previewInfo.heightDp.takeIf { it >= 0 }?.let { it * densityScale }?.toInt() + val isLandscape = preview.previewInfo.device.contains("landscape") return Paparazzi( deviceConfig = deviceConfig.copy( nightMode = when (preview.previewInfo.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { @@ -131,6 +133,7 @@ object PaparazziPreviewRule { locale = locale, softButtons = false, screenHeight = customScreenHeight ?: deviceConfig.screenHeight, + orientation = if (isLandscape) ScreenOrientation.LANDSCAPE else ScreenOrientation.PORTRAIT, ), maxPercentDifference = 0.01, renderExtensions = renderExtensions, diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 diff --git a/tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/features.announcement.impl.spaces_SpaceAnnouncementView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png new file mode 100644 index 0000000000..473fd3f275 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Day_38_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26071a6b16f4446526f6abf28274580eb0f385bd0e74f4f2b0479da5f5d4f6f2 +size 12914 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png new file mode 100644 index 0000000000..14c08f0b62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomSummaryRow_Night_38_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ddef7c50d6a791c6a32bae8506a999b198bc601b70b6ffb5e5e7a8d5a2f1543 +size 12869 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png index de86c4285d..03d7d97186 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d69d687090971ab7e9b87f959521109fbed48413ef217e1be3da8d7d985038b -size 38751 +oid sha256:ecd79d3f8c4efbb54efa6a5b8dee79274ae9957b01146025dab42a45fc328fc0 +size 24740 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png deleted file mode 100644 index 03d7d97186..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ecd79d3f8c4efbb54efa6a5b8dee79274ae9957b01146025dab42a45fc328fc0 -size 24740 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png index 07e9ef1e28..049aa93a8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a6a7f3188eb9635ade7c0bbc2239257cfbe3166f4a6fb7917a1e5578f51d9e -size 37436 +oid sha256:0fd37517b8913e2bac3275f717aa15bed18055a7525e0404b25b43cb8bd8422d +size 23840 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png deleted file mode 100644 index 049aa93a8a..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0fd37517b8913e2bac3275f717aa15bed18055a7525e0404b25b43cb8bd8422d -size 23840 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_10_en.png new file mode 100644 index 0000000000..8c6b1bd7cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8ee76c2369a9671cbe370f367718fcda5bb08a89ed5116accc96928a64e9724 +size 55689 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_11_en.png new file mode 100644 index 0000000000..35f633b3a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029b19808dd54b74ef30737242a10af31b80e579b313d001dd9fa377bc2cca58 +size 58319 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_10_en.png new file mode 100644 index 0000000000..7983ef5e8a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21a24fade9819efdb9114ec0ba3db21ec87cf93e32d896e22117fcd4f23e07ce +size 53601 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_11_en.png new file mode 100644 index 0000000000..1c50d23117 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eca54577cffddb66921623ede7ab39e017f5cd95e5049d6ad2763fa4f1f88ad4 +size 58133 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_5_en.png index 69aea3df2a..74d6ac15ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ba1418b5d42a56db47e7cc574cedb886c75d9cf22828341bd954a4f1845670e -size 17925 +oid sha256:2621fef4175ad0f0982270284ffbbdb6b3b0534b09013c1cc378504a85d13068 +size 22385 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_5_en.png index 060f25819e..d79bfca142 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d786937d790e13b53a5de8ae3321e5aa8744a979a3a99ecc36def6b7dbf6cf60 -size 17237 +oid sha256:69bcf19161f70c27256c1bf0e5b99c993c86dd3e77a42d0e87061730a5c0c752 +size 21649 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en.png new file mode 100644 index 0000000000..adbcd16ec1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d861dd7397c0e15091e022d956c1955d86529fa0cc39e08c3c645d91e5023e0e +size 77677 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en.png new file mode 100644 index 0000000000..df7c95510d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0acaebca757642346f3381601f044a55d02749575150364e232f772ba0167e1 +size 73811 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en.png new file mode 100644 index 0000000000..7bed006a97 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54c841ffefb3d053bb74e6f88a9aa7cff5d39b4854c92de3b507552a9a4226bc +size 68693 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en.png new file mode 100644 index 0000000000..94a1749517 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eba1f4395b4c32e7edead84b1e22957cc8f973121207d7000419a8f4c314f5b8 +size 65936 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png new file mode 100644 index 0000000000..4a5a13a36a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ef35fd3f5346870f11120a37c9db969453b7594bf9a0ccc71fe43e7fdade488 +size 62532 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png new file mode 100644 index 0000000000..1b69f8f6a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1501c2591f7df68404285770b1dad67360dddce074d4ce1c71223ea0baa0d1e4 +size 60873 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Day_0_en.png new file mode 100644 index 0000000000..e3e5480add --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e73b036458ee32e207f711cf6656fe7646b23d3d9e096e62932c828dd53189 +size 5244 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Night_0_en.png new file mode 100644 index 0000000000..6582264383 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.classic.root_RootView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518818c549548b6304d2960242ce7251bb609fa439928539a7556c33223ca8ba +size 5251 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png new file mode 100644 index 0000000000..133535c6d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01fa1c9b917b65afc2d1464fad177f7420dea1625eeb7c8335d8105664134e67 +size 312145 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png new file mode 100644 index 0000000000..30d5478a90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.onboarding_OnBoardingView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e622d9b43664c5a31b83b41801ed07769384ab9ab84aad57605cdb67b16c58d +size 392254 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png index 6e14838915..986547c8c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31e730519a460ebc21ebee0d24c429ea22bceb164bd99080fe23b2c1c010577f -size 22488 +oid sha256:d2acf7cae297e8be76765b392dc07a36692a9e597b8f205de31a04dcf8b6bdca +size 37918 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png index b83a34aa0d..d333d2439d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:426d109d550f6298f9375c4a8406210b7d2c52a590678e5c21d4a0ac2864202d -size 22560 +oid sha256:47b4d158df0f83631d099c072b8b3d95a57d539fb9e8d7e6802f6a90a3b78284 +size 37904 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Day_0_en.png new file mode 100644 index 0000000000..ac6836af2b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c523f3a502600b837c07ecd5804831da2d9aba5a74886b7001affbb90169112a +size 12455 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Night_0_en.png new file mode 100644 index 0000000000..05f2571669 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadListItemRow_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528c2f3183a153b9129606806fc457265819ececd71cf5021d1d843970c0b774 +size 12366 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Day_0_en.png new file mode 100644 index 0000000000..adeb4f7c4d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c02999b2d0eba92f1e9b1f5fac52bcff303401f177459d1a439b7db906e5c2a +size 64622 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Night_0_en.png new file mode 100644 index 0000000000..d89f062c11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.threads.list_ThreadsListView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7f6b64bfe0b47546a009efe23060ba091adbad32288ce546cb3b030d7a9ec88 +size 66177 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FloatingDateBadge_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FloatingDateBadge_Day_0_en.png new file mode 100644 index 0000000000..73693b6b51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FloatingDateBadge_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8109af7397a43f24a27b7c988f7b6bd23e555df038ff9a272068707796a60962 +size 8465 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FloatingDateBadge_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FloatingDateBadge_Night_0_en.png new file mode 100644 index 0000000000..042d482550 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FloatingDateBadge_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25ba3336d226da0fb5956750b02f389c59b81c93e6150d8fb17d5195fd161204 +size 7705 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png index c70050658f..0435f55de6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd81cbc3db5d329fcc1f0d4866c220af7488bb68929d0e75b983a170d15a7ab7 -size 380173 +oid sha256:ff839731834d7d0c94fa9a2c412014ddbaeaa053c5af1c8cbd00a3f3a583f364 +size 379511 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png index 57e2226f1f..f0b5506b89 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3be2bb0c8e344780d59b67a4ba23e50890b24a8b8ad1c48673e34403ff8db496 -size 378130 +oid sha256:04ea6c2947a21d3d4a3d43e9c4750c4f2a49056536a4da3f34c95df8353e9de3 +size 377990 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 662ccbf182..3ac59caeea 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da65fa129fdfe903f3e466f4e1dc6c70a6930e0388a3f6d3fb116e5b91411cc3 -size 365497 +oid sha256:1a94d5198e27338675c0e15df5874ba3272ee315b8d77291dc7ccc7c754ed1f4 +size 365082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png index 11289371f2..86018764ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd797555bb72d7bc1b874df24ca5793df5550b235fe9a28d20aaa04bf5f3d0fb -size 370592 +oid sha256:8344427a0fe87cbf0883ff53a9ee4dcf90976a7011f6618d85961df48b2c53a9 +size 370228 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png index 64bcaa05f9..058a3992c3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be969a809f346799887fe031e139dcc47db8bc851ca4ccfae8dad4ad9dc523f7 -size 363498 +oid sha256:00fe1e676086766a3eac3e4457eff52c989789fc5d8d0df2624c019ed25ce9af +size 363322 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png index 33ea565e67..db4ea8296a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48c0af64a6aab3a28ac0493bb86cb92424555d6793a0a5c6cd446c25a20ce915 -size 368757 +oid sha256:2f0d24b8011564ee7a8b2f8c2e6a24eda668ee42d451d862f884acb7926aa06c +size 368529 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 1a345fdd13..94428abb56 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b4bef0095e989bd411426a2626155f1b75931f05c32d2632144f3c232f3aa93 -size 348379 +oid sha256:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 +size 347897 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png index 7ef8d729ef..7ddac02717 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30db6503b7dde29553b5f806924565b1d60c13edeeaa982dd2ec7d2a5c8f1162 -size 364508 +oid sha256:a4ac79f3b8fb4e231244bd96b244a4f2526d6ed28d558f9d50734d83bbb74992 +size 363909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png index 72f5c9a9e5..bac23aab98 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15ad31eb2b457790913442d3186fae215f77f345470748cd64a345849b9de9d0 -size 346837 +oid sha256:e30686c7c037ef102717e0316543faf4d3ed2671c21eca96a667150daab380d6 +size 346614 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png index 5a6ad7fc23..b6e4650d9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e72a79c374fc23a1325e1bd294584b8b7f0b312c804b932144bc2e5d8911bd9 -size 361950 +oid sha256:2cd357a1fbaa425eca95587bff2f768a45e773f983f75664875653ba1900e534 +size 361625 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index ca682211d3..67490ba700 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:092a0d4c37d47c84c790a767b9acc77f38878876e609b986f80e5fb461e0a340 -size 370156 +oid sha256:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 +size 369730 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png index 51a93f68e5..f0dfbc3321 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:097fe6208d9cadc707e6866c151a2ab843550a9fab4d2cf4908514ceafc4e396 -size 355327 +oid sha256:f7a6302320bd1e4258e03487539bf46b0b12781dfc8a22b3f9f267017a6146fa +size 354751 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png index e60716fb3b..c4943b892c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f61a7a62c6fc96e23c8aaa793f5fff68e2197a067ad8f1614d1103e833b3230c -size 368585 +oid sha256:4c410e853089cc2c4a2c48a00dd9a8babc8095aed72bb4625057444710559c6c +size 368129 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png index 73be879502..68f8148879 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:091352ebd6bf2abfac5dfd99be5edc94159a5a45c1d3137c7271e0031116a254 -size 345649 +oid sha256:6ca8cf82aa8ab1bdad56a27584b27789402aeb7ad8fadd038280a376fb75a72c +size 345094 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png index 4ec235074c..8aa2cca147 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:842632747c13219ee6c7a50d9003c9e07c622393ae4b066e86eacf04f807b181 -size 358344 +oid sha256:02ecb903124273e24151ea2088f6b99144f7dcadba00a582b6210dd23835b5c9 +size 357743 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png index bc6cb62827..cececc229a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f43d615fd1aec05ee483d3047e99c1ecc89a6e5a7e56e6cf7e1ea4c108be413 -size 357473 +oid sha256:f0c6dafaa41849af00d5097a85bd88e3a054a0a9e41c0466a08e6ff8dd6102b2 +size 356857 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index d39b6ec114..14a40a5676 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:898a1e8f0277d43a1127d99ec50687c24fe1154ceb290832a14fa5a43f12647a -size 365153 +oid sha256:a11044fe90227f1f316a1ade8587e545bb620e11cca76865fc2eb83cdb817fc5 +size 364655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png index 0617c06d2f..133fd3903f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cce6211c0a33407498145cb03a780d80553983146f487ba3e26f794ddf12369 -size 397594 +oid sha256:426d1c844a8c88281c5fa4c61916d93278fdfd38f8cdf2dc54bf1a821f2de3eb +size 397167 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png index 838c70dcdc..0041a870b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7e91b7aec4c43e2028a2e69a02f66e04f6a3188a42387013e3deb1faa7a108e -size 356627 +oid sha256:a2bd77c08be705c66225f16c29fd6826decd92b41922d0f8ffc0d1a63d8c59d2 +size 356193 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png index 44e5341280..9fcf389708 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80c2e99ce6f851994da38077acc30ff386867f2093a8db4de3c1b82b0b6c1a8e -size 356307 +oid sha256:9daeee1aaa04247da3c8bb5aaae9947aab1ed2d2057995e53d6ebdb63e5cd356 +size 355848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index 6a0ee13891..d235670def 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4fd5470ac81fbdaf471e297c0f5bd9c6b9945e006ab88079568848befe1049d -size 365366 +oid sha256:a00bd766a00b7e7c6615fc53bf469b3ca5056a6c514c096431ae38d99d604306 +size 364845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png index cede2d333e..b10f9741b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a75fb4510d405b29b120fa400d4f69a89965c11a2228b5cc2e772200a12227e1 -size 355912 +oid sha256:3ec1b0244dab22414c6719b3309426e13128d0c70fb67dc66cd8a60d42521fd0 +size 355411 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index ebe440bfdd..422c324695 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3f7e96bbbb4b3661514713514d7f6cb27c6634ba2621c22c0dd4ff730e5e24a -size 368043 +oid sha256:a1df8d22766981fc76d6d767dc3f40d98cadc7c51be20417b067b91af604232b +size 367884 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png index bd841f6f75..1d34761a9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:449cc2fa5ce39163a5d5eea3430c7dc457579d41b0213b8b46a61acaa76b3ccc -size 353304 +oid sha256:f7d9b5fc0ff5d7828ad6f4dc0c9bd9520b4920553da97ed7b4e030ee844c0208 +size 353054 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png index 2867c5dfd4..c081fa16a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cb93499ea8f21d3fb79aa81b2746972f57a4732296bb421897e93a47ddf7d06 -size 366544 +oid sha256:6a92bcf5885322e11a706071969fb674b1f6e60e021af153aaa125baa4a602ae +size 366285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png index 83d0f043e4..b6d1f07279 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6bb6a35c2e1d2090e6e6e5b3a1733d16fd433e63925082b2ea32293ecc639fd -size 343140 +oid sha256:4bee12a6bdb18fea5ac6997671bfcb903296da7278cf69b2456330066ca31dfb +size 342870 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png index e178f75d91..ceca184bd7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1067826b5b1e3220eef3ee3b9a8c9f77bebb248ba2378cc2e7029b86c585c48 -size 356339 +oid sha256:8e82011a72f36889c53ab61ed2b9947c7fb5f868c9b7010904fc10f4ca587082 +size 356082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png index f43199a9d8..5cc5cfe4fc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:437f71e1845447b8f09905b4d97dd46677d8d8db669a317f8cba9ada6f5f1aa8 -size 355551 +oid sha256:6524cc87257cc9d3f135904f4e14594b56df695e71fe576418d2319c472d710b +size 355285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index 80db679074..e9d85990e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffa5216fcff101f8ab1047ac9808f98749144e1b1197bc01905cd6ad251d31b7 -size 363081 +oid sha256:66d4b6ea4a697735bbb0fac936b89cde99c5077973ba828894bf50b5dae233b3 +size 362848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png index b7a7e2020b..0eb106f4a9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:222460ba0557c96c2ad0e8aab675a254f515281d06bf4a7b642ea7ef79d5171d -size 395589 +oid sha256:217ac95bf476c53d2dc6d3d0143b21312bf38b8ab86c8dfd4f9360582b584f9d +size 395476 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png index 75bb1086fe..ae8495618a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0a738e57b657bda2454526dcd78a37d38839183a5feae99bf7f107c12ccded -size 354661 +oid sha256:08057fdc1e1184720b438ddd2a8753f00791baf2456cc30b45de8e6bdeb57e8e +size 354404 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png index a93768052e..40105c1498 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f98146a417290fcc741ef676fdb60f36822931358e943ad12faace7f96e995f -size 354422 +oid sha256:5ed24d190f10c079f8cf176404372d40eb25cc78ce595a22febc540315057b86 +size 354075 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index c09d98903c..0cfd1dc985 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54c9d1f121d83f1a161f6483a6b4bbefa220c235ae88097ed4d03baf10bbcf9c -size 363289 +oid sha256:545c9e8933e420cc3f8fbba0fbd5162c946c65a20721faeb77c568596ca720cd +size 363045 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png index 0e2c06b0f3..c8306263a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:932e5b29f2a64d70956470bee1be397775c6205f590081d3b90c98cf47845577 -size 353993 +oid sha256:756f0cff90ddd61adce2f467c0384d74d430f6d51c091e4980e45bcc9ee6f0fd +size 353705 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png index 8c023c5e17..b2c131b124 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3136c95bc9134eba4cfdfe8d552473a54287c356bd3895291b9cd4ec11969d9c -size 52706 +oid sha256:e26887bd81e10726414e1833029b4b51e22e534684a230777ca50f86024af994 +size 56430 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png index 3b3133cf67..41b8b16bcc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_MessagesViewTopBar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6770e720477c2547f593c626cfe3bdafb9b7c78d0b66e910fb9eb1163730045f -size 51707 +oid sha256:ce44cf850169736008a3f3fc21a2be4fb044badfae631b0284ce379b325879df +size 55533 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en.png new file mode 100644 index 0000000000..708313c475 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62068492969ad00e1a8e4a44189f93d83c98fee40612e4eecb68d3076d00ed07 +size 56425 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en.png new file mode 100644 index 0000000000..b69265cf41 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d92e220d675097c37b60312ca4c4e821eb6ff6475bcd004437dde0d2e964cce +size 54756 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en.png new file mode 100644 index 0000000000..b56ed34048 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf121c0ea1fb3cc7a47a292877535c9c0a1ae1ac11bf7f5ce169d0cd79844246 +size 53775 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en.png new file mode 100644 index 0000000000..d0a5500209 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37ae25b9f9c659164c006c9bdb9f94379398ee8215e8a2dd9167620ef994b6d1 +size 52347 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en.png new file mode 100644 index 0000000000..d2929c43be --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db439869d2b8843cd58251bbca638e0dad7ed2b1ae4b22507e052e3d5521214d +size 52090 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en.png new file mode 100644 index 0000000000..540b910b54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cae56bb14ae7bc1f6c73884c2efb333b7da22c82d69026854a59c380657a5bbe +size 50705 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png index 4026c0e658..503b9ad61c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e24299f25ffd7095887bb52214c25de8de2cf0380b374d11f65d78c86f49dae1 -size 45507 +oid sha256:d18a2e9ba19401f958483bf06ecd7b311ed7383de0df3d89f3bb4d3a57f8e9e7 +size 54191 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png index dd9d30850a..16be314a0d 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d84781b107e2f25bdc88cbfe84a1933dd20bf4c1dd372cb69f136f36df2607c0 -size 41951 +oid sha256:aafbc1f791f067fd0084db3bf293511e3cf7557329e7599c3f3e3c66f01435c4 +size 45472 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png index dff4e9fa71..503b9ad61c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:577c00e6e45e1da5ac1b1deee380d7a087b1f32e077f8e5b9430497bf6f7012e -size 44083 +oid sha256:d18a2e9ba19401f958483bf06ecd7b311ed7383de0df3d89f3bb4d3a57f8e9e7 +size 54191 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png deleted file mode 100644 index 4026c0e658..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e24299f25ffd7095887bb52214c25de8de2cf0380b374d11f65d78c86f49dae1 -size 45507 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png index c8188fbaaa..430756b5b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9750c0ae52dce1ba63c1cff7a22a3e3c75c15b5a556ba7fff49815b55836372b -size 44198 +oid sha256:dcca87aa6eee7e45bedb8f58c3b776e3932d94843573eea70aa686c3de82e03f +size 52290 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png index d3c89a0735..4225562f66 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31a3e5f9abaed21c87052ef7642dc8456d75580b79988ebe271f09d1381e9a03 -size 40820 +oid sha256:25e311c9bd46defd9659004d2d36088985c0578189a41906b51bfe683ddb6488 +size 43889 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png index 5bfd54ce11..430756b5b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7aab145e8ca2cd9de64a145c7966420a474b3500016a46100dad798f33acba9 -size 42792 +oid sha256:dcca87aa6eee7e45bedb8f58c3b776e3932d94843573eea70aa686c3de82e03f +size 52290 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png deleted file mode 100644 index c8188fbaaa..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9750c0ae52dce1ba63c1cff7a22a3e3c75c15b5a556ba7fff49815b55836372b -size 44198 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en.png new file mode 100644 index 0000000000..8517440a90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4dbcac95e6fd72ee2eb683a4f25d49f4d181be9b1386ec01670fb078bc46a52 +size 18966 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en.png new file mode 100644 index 0000000000..75ce9869d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:901c40d524de6de6fd70cbfcfc8b1d86cc896734254452535a07830587adafcf +size 18987 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png index 035b39c739..9b53e56d9f 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cdaf746151224c1889c60232b55cb8c9d8b3c07cd341b1bb8ad6fb7abff97ed -size 9538 +oid sha256:a7983d27647e60d385703d5bb91763ab787ee69e0a0ef04acd13ca86d5d5b1ec +size 9560 diff --git a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png index 2a46e52aa3..ced849e58a 100644 --- a/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.viewfolder.impl.folder_ViewFolderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:072d91aa19fa0c5ec80ca565e1529988598cd54afcf6f593c6dcf17882170d0c -size 9393 +oid sha256:c32280e645ff1459b179423f67077dfc75093f5e6c40fff659769d164f391aeb +size 9408 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Day_0_en.png index 8dda833412..c049cff102 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f3df9c9de000b15692b982c7d69f85ce3d03cab468c342773c88eff3b4fca65 -size 9082 +oid sha256:ca6415ca7f858146a4c00e2fa1d9602fd9f3d42c0c8ba9830e121769af80676a +size 17255 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Night_0_en.png index dac3588ed9..8074748111 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_LocationPin_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9767298a096be7fc78210245e6b806a72a6c1ad16d7335e0f89728bbcf08ebd4 -size 15884 +oid sha256:d89b754907f98ef1df2cf227ec535cdad91dc15e90b4915039be39d8b2ae2ebd +size 16721 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png index e3cda24208..79992a9708 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_AllIcons_Icons_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca6ffe10dd122a2c2df99160b4e318e591ef6d3b10b6173d36f6c9959a93277f -size 114895 +oid sha256:30b39281fd597089c94c8ff1680297143989d6da97d6f5b15198be5f92469483 +size 114653 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png index acec20812f..c7e3599c58 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c5a4487507334ec43c9d659f57f2ec0d86856d941f8b1b437c101b696a5b49d -size 24223 +oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5 +size 26077 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png new file mode 100644 index 0000000000..d7ff4a1d2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26bf76ccdb56d042422553f557d91d0f26d874a710f696ac106c5c2b5590d332 +size 38833 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png index 0c60a3da07..fe44b8941c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab0ba9a693ede4106d09170710f215bccfd82dbfbadfdafa5fb49fe39a03c25d -size 23471 +oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41 +size 25232 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png new file mode 100644 index 0000000000..f5ff7856b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8413aed02383572cfe8c481c6ba8b0db4cfb3402334c37f2d8b54a73fe4bf594 +size 37343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png new file mode 100644 index 0000000000..60c5e812ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e514d7f36fe25150d66c2d2092982a696196a5a0a2674eef97b7231c0c03bef +size 699410 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png new file mode 100644 index 0000000000..5a96554c0a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:712af2723a0656ffe927f1cd488d3117f5b450fa405dad307b0c88a9f2483f9c +size 698704 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png new file mode 100644 index 0000000000..e4424188e6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d6ab1331e65a4d02accb4d20c899a06c37a022d9f9080133799e30763969300 +size 26774 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png new file mode 100644 index 0000000000..3dccb4c068 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:010120cbcbfed1e7bfa3fb4c88df6e1098d0382cb1834f1a8046e312434f201d +size 26549 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png new file mode 100644 index 0000000000..d0139ec184 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52174b55b1737787260a454c59f243b0e3f6327ef5ec71464744def928d165d4 +size 206785 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en.png new file mode 100644 index 0000000000..71ca7c633d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47c2e2194283803c55553c0220400ed8991539f25b772e0dbdde6d5defa3d3d4 +size 7343 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en.png new file mode 100644 index 0000000000..4e8d03c6a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e56f491ccc4c9ab82dde36a8fca588bdf037fb59b70ea47ffe13a56e22f801 +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png new file mode 100644 index 0000000000..a32a45029c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99478fe3d9d44e9cda39f33748c70017203a2cce5cb855605740a29360221bd5 +size 184910 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png new file mode 100644 index 0000000000..a855818ac6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:062f8342a3a3715498ca34488c2d9ffe09c1e6dbbe04df09e88fd107a33b174a +size 653228 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png new file mode 100644 index 0000000000..accbd26985 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3effa629295c12d2248924cf298599f3bd28975ecc76fac84f771c16266d738e +size 698895 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png new file mode 100644 index 0000000000..b9dabc97ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fd3b7065860d1e51fe51a0a6e3f4a9e61a77ea91cefe85cc3aa3560c4cc6bb2 +size 252253 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png new file mode 100644 index 0000000000..f11d4fc3fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f9ee78e2a034f34d77a438d305a5cc63ea583df80f83a14b1d81fcf74ca93f4 +size 665416 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png new file mode 100644 index 0000000000..5e081d3c4e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1e91c66363a5af74d4345f7837db178410c078e390b0cf9296d0ba4b3dfd7cb +size 207004 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png new file mode 100644 index 0000000000..d61e0a50b9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eb950f3c0d34a796ebd7635ff736023742d5e6a243912f3ad2234ecf08694b2 +size 183220 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png new file mode 100644 index 0000000000..f2352c88f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f6c84fbd21949a3d9d355bfb627b8a70f65fc669cb6b6818a325fe4577351f9 +size 196092 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png new file mode 100644 index 0000000000..bd37e3eb79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26bd927acc578cb3585f6e101c066f0a6adb6a6e424dcd196388d5a838d8b22b +size 196396 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png new file mode 100644 index 0000000000..69515d4326 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c4440f5ed01f2c1b405bb2d444c52455ea8ee1c094baf7d2f85bd9a9ff98b02 +size 210117 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png new file mode 100644 index 0000000000..5d254988c9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11f140569061e21e6b546118b57340a0830779580498a31627acb2e6c7313471 +size 210490 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_17_en.png new file mode 100644 index 0000000000..dc06b5afc9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e353d24c2b9b49abaa11e064b1581215ad65154d0e67372fe24684ba2695a0e +size 442063 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png index a5db879ab2..1a6f5d660b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9775ee85b895e1eb6b58841a5232de980f44bf4a20c0f2e2bcab731f503fc762 -size 11000 +oid sha256:30ae0b86a9834a90990a42f6bad0fc9010b4109bfbbe85072f00d61b0664e6e2 +size 10926 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png index c186a409ad..1c65fadbf4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7ab6bf5d8255d61569c4400f5a911400f415a4e228de41e181ccb42e2742e47 -size 18637 +oid sha256:d6c87eb2b2da4afdadd949bd39e88389c34053adb19d34f8958f97ae1faf90da +size 18557 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png index c229dd85d8..42f4e1a941 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c54410a3733a3cecd1b161eb524e1809c270b8d2dd814c095eb73e013b486e0 -size 7840 +oid sha256:3c49dbe21e9c465adc1f12a57b87dcb06569936e80d047e0db77314cefce4691 +size 7756 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png index 609162f4bc..41e20b4266 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:793eea182afcb72be59a090d3c3e622edcd6f9509271bef2e75a2f45f47a8fe4 -size 10567 +oid sha256:7523f982565c692f9986d11bba14ecc51a3401700de5a752fe5d2b667d0ea236 +size 10560 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png index 874da23636..be3a01d3d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e1cd401ab146d2d15e94a591625ab40440e32030f4acfd4ed9bd18dce01a1f0 -size 17814 +oid sha256:73769f2c3ced2e20f6a3a91cd8cd040c14e93a19c923d5784e2081cf4baf2d44 +size 17790 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png index c4e3bc9a39..90d85686d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_ComposerModeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e47e98d0ace1eb50dfeb9935c27206abee28d42d0e092d410de4239dc6ac293 -size 7519 +oid sha256:ef3e1d6d11520f6475059032d53c9c0a6e80c133318a0ae44dfc67d9b69c31c1 +size 7494 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png index b51a04e421..df3af23261 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cf5e60d289daa62dff1c07469227acffd275a1e4a3534b60cbe37c89a2c4bb7 -size 72136 +oid sha256:2703da4167bd0198f9666c2160bf95e4c99ac718864ae67d277fbbb4fd50014f +size 71876 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png index 217b3181b7..6d8f6e1a3e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af4895f8222d984437a5d7427268d198345ad690161789d7bbb0340eeda916f7 -size 58660 +oid sha256:86f6e18357569284c223afb87195be656e6e6c90794e64ef6a13346c8de6c496 +size 58367 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png index 046d651b57..2704eb3847 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6049adcc9de51802804d0224bd7d5a7ee8ae2a754411e4ca5685df8028d51480 -size 71727 +oid sha256:05f9317281ed84751b9f168c4495e94d1f68a9f9e7946270402db59e259d1ac5 +size 71392 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png index 26ba390595..6cc38e5ad5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e339b203a782f7d73195b1cd6961c67dac8b313a6a965515e4bc2d2b83a2118d -size 80524 +oid sha256:be4cda10c2b416d70d020c46abb00e41a2802e5d71f2dc416c506cffae147af1 +size 80226 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png index 0a18ae1074..a74e2710c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22cf555cb18cea56f2e6bd9f91dbf70353916f196ceba84b11ea017b9e95d794 -size 61511 +oid sha256:3d29017d9dca92779074b963ba29681bb6cc83728a15ba2f55846c8fb2751d1d +size 61119 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png index 063006fb88..f3472de5f0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1e98fb04a4188e2c6b795a3cf0b21129d0e99f8ea2748137e9f8122d413c6a -size 60353 +oid sha256:a8d1701f1be6087e93d15bbeed97659e57e0d58b9d5087fad0253d5d3dd60194 +size 59972 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png index 9bfef2c6a2..be14ebd9b2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1861fe30f2c653911cfe193d69c21affea73f458132d6a1b6381aa504d4cce74 -size 67327 +oid sha256:d2d984f17ca414c523d6a2cdcf9fc4f82c0759b22b72b60e8f5bb28776350a72 +size 66958 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png index dcc6b6c915..c0ee61b312 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b5f1d42f06c4c5949f1fb1d216c7af45c4606a782178c21b58d0a0e7504a333 -size 89006 +oid sha256:eeb25a0b7738b0a8b12a2fbdcfc469dcfc2c893ebfa4a50a3047491f817ff981 +size 88665 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png index df1c374209..05cb3a4a89 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7074c637e0a3e9320f86a4f417931d59c2d5e199996e8e533f692a82ab0d8d2 -size 59581 +oid sha256:f1f008fc8bc035bf9256e19e831691d3ac7c3347e83aaed2ae8d4b7a7f5b4213 +size 59285 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png index 442ea4ff99..63622b0027 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0f084ce1bca37f796e7f50d78a4f03485b77fa742efd688d6b8accc2b41f5e9 -size 59734 +oid sha256:e9294ee360463d72968ef89eeed7ff52c80da29e4a59f36f00422f1fe3cb7d9f +size 59350 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png index 4e4fd5476c..62268cf4f8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20f92a0cbc8a60bb057b131e17300653ce21367627f611193b6b9ddeba5d4b37 -size 66908 +oid sha256:5dd13cfda2b0f029a46477cbe9f8db1e3a191890072665adda02ec0750cddcd6 +size 66535 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png index c5324db3d6..da1080256f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a0be3b08f1923c223eb433cda9d8c1bd86ba2dff1bce31e069b29953b13a084 -size 59085 +oid sha256:9b32f9442e9088eec377f74127c99002aaf2c5668b0550047dd7b1c9e73ad619 +size 58783 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png index 8fb1c22b45..9e1047504c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17bc1cac4537bd0b9a2dcd91838ff7c20de7e6076dbb9b174ad801af76f234f8 -size 69080 +oid sha256:24baa66c2fced922b328987b51c1045af7111fb3c7e4bafdc46046eae61d0f87 +size 68773 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png index c6b221f461..70a49794e7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:caceabb04420f3675940e3b95ec685cb2b02c69d87fd19843640a5e882dfa1ca -size 56124 +oid sha256:e343b71b9f170fa7879510544cfc95af1ba15ebc128bc5d78afd49d10c70dc66 +size 55802 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png index b5e644918e..c04c1cae02 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d46716354df7925d15345b61d8e16d80c0b3552b687a896004f4174286ad18ef -size 68625 +oid sha256:d76f37cc5bd2d6ee9b7c87bfe7b656b0095ce6ded4d07cdc48d255039aed3520 +size 68313 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png index c2bbd56d35..98a5dfd03c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a0b4ad61421f6b142e51200fb40df935027962145fa09b9871a11cdfdea8c0f -size 77316 +oid sha256:d24998e960d2d4d36c1ef0f589e9094d972203016b0a923268c1894bd09f267f +size 76999 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png index 3bb5259f05..ba691c8f65 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89b7d536f86c72378714e4fab14b24671a86726abdcd63a1e15abcefde6a6abb -size 58721 +oid sha256:62592587ff02f0d36cb40f0b59fa3e7c6925cd42622ac76c4d561f6df999eba9 +size 58404 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png index 4af97d2cc8..7ee8d6355c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d314aa8f8572a099c4aa0f96de8dfade75c57edee4732c3d1300e5356c0bed20 -size 57649 +oid sha256:fc54d34649b067af6e0daa7d2e1c8661572fe0139ea9aed209b28c3a99f44f12 +size 57353 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png index 918c7fa1b5..80a0c981fb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf3bc1027fdbbb194d1a0120123872f0ce75c41c43d5c73c72908ddbffaaebb1 -size 64314 +oid sha256:10f02c7de0575dc30c65cafa4adeb265d6ea1e487db240f1d4052be138a8b0b0 +size 64014 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png index c5eb73aa1c..3c4f0dff27 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92273288bdfe895fe0664533f60e322893ece33d271259303e35d9358fbae5df -size 85238 +oid sha256:a196fe9e7c551fa7cc1145c9f8df12c5905678fae285884b71e7ea15cea1bf5b +size 85088 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png index 4449d490db..616dacf9a0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d04a904fb5f98c075ed844668a06af29d03e7d1adf8bab7706d1cea39abcceb6 -size 57003 +oid sha256:60b3b04167249a6283dccae1450645853df5e05ac7a31f419c674be0594dd413 +size 56710 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png index ff54d6d0c5..256094a264 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:896b4497818ded4ce18ad6089fd436160f87189d79da1cb63ea652818de10041 -size 57009 +oid sha256:d303c75fe252ee9d3fb0bd91131ed44b731ea6fbe120dc1379aeac4c4d43c467 +size 56693 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png index 6d72c0f7cc..9aa831dde8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:256349593b50a7e1f40db6c032aa39a640136934eadde8883936b0b1885f81b0 -size 63867 +oid sha256:81d23de4f3afac7993f42ea8030f6a2392768b343761408d6737382492443267 +size 63586 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png index f05bee7bd5..3c59d53b44 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25b5d39992361966545a3c04b3de7b20569a1e12800b3ecf38e090ceb2afa7c9 -size 56567 +oid sha256:7c3a1636c58e8243f971f69ea3575e75a12526863f1304a32f65fc2991fd403f +size 56250 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png index d9d7e2913e..93c1f0a9ce 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfb2c4674cd8bd0bcd434a2790f3e121dbc6a6c39e41d0417c80cdfb44be91ae -size 73627 +oid sha256:8eb6049cc99813238995f936022fd9fd5a43d1e917bfc69ab1069fe3da456cb5 +size 73316 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png index 7891573b3d..fe7b50f2e8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be931ad71b0562cd96456124c987ed79709f3ec7c55d736f4f71da1fdb036309 -size 56863 +oid sha256:611527e8c0bce7a9fb48b5eff42170cb9da5f40cf020c78209dee84615d300ba +size 56514 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png index 2e5b1dc647..dd8f8d2a66 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3f24723f76f444a6503ee01399298ae4258a190cd6657dd1bf5308ea528d55f -size 72035 +oid sha256:77b28be1b95709082e0302cca5b65c2d57ab48786e6a22facb8c86116f058b11 +size 71667 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png index 584285c1be..be45e720dd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb33cec43d32917cb8f59e012244fa0c8824421cbedac516e002862f28f93db -size 83506 +oid sha256:31875ed4c6d9443418899de47eeb5b61d2246d8321e83579681cfa152ff4a056 +size 83174 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png index 7414d959a4..e7efad21d5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94d01e112a860c145c6c09d1d3a9bf948fe73e7b939d7f602704a3b4853de9f5 -size 60076 +oid sha256:5cc4f388f677bf04259344b5bfc9033ef6a4a76ad161012abcd0239ef520a8d8 +size 59670 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png index b6e7b4e2a5..638e2761d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b064ba533e8bfa225f0548fc6e13d811a0ab0dddcd500cfbb72ed199a268c1b9 -size 59202 +oid sha256:e57a614424396e36229d7877a0bb222a51e9e955a42e180acc1d43c048684da2 +size 58787 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png index c223cabf8f..6c510542bb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e0cdd88f5776e92aa0063c09f4c7c1c099d2b8cb0f4a13066925c1ca105b37a -size 66800 +oid sha256:4546f32ff1eeae0a923d473491926a6037eddf59c0c88c516c504b9875874c8a +size 66402 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png index 042c2792ce..d472874510 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa0e313a3d429ddfba85b62f4d855489cb5cca62bc6693dcaa33f4d0ac622b85 -size 101888 +oid sha256:acd25c00d9127c2c78f48b22296a5ab33f1e16ec5698c93dd29adc9172467aac +size 101706 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png index d1b0e795b5..48f0ad6881 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e3210208348505b93f4d673977f237bd7859b054699c1d66ece28591d20640b -size 58145 +oid sha256:d066e82aba5742c7e84a260de0e4578bb3bc8faac097225271cfdb14cd52f5f0 +size 57816 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png index a014be7a5a..f8f0337792 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0c9e887cd48c5aee046649a2ca5949abe763363659e29dd523869dc615e98f2 -size 58186 +oid sha256:2c037b4de113387ac7e28d256d28ce9308be184b4ebff988ed96760623acfafd +size 57776 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png index 4f07ac8b8f..6278391831 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5febf4de05a7794160b567dec3b00e818a6e3d4d7830d3dba97f72498aa5a3c -size 67172 +oid sha256:81883325e2e450c33f38b3143c43a3fa3d659a210ff6e9ef40de71b1d0522f50 +size 66781 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png index 2d5e2c39da..07c20262cd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f213b779238ac8536e4f0115d8de444a21c3b8ce31aac8aebd5415d8f80504a4 -size 57501 +oid sha256:b6b5808bdde620cac3fe980aee1f8162a854965113d99d074442a2baa818d712 +size 57198 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png index 362033d451..c41f41d043 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27cc53e2336c9ccf90d97d2aed98f94f83315dcb9fe4bd489958b8df2366262c -size 70207 +oid sha256:7174f340d6e16be525da9a06270f07abc7496fc6c41a5670a71bd1b23f4a72c2 +size 69957 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png index aa712c48ec..5d3b894ebc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd6c1a82daedd923982d81956924eeaa37857d54dee55f31fda4520071b4f66b -size 53675 +oid sha256:dbb0c22089c063c18042c8778c1b69edb441af8071f7dfb9a3ffdaeab4ba4c71 +size 53340 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png index 804b238d8d..61a6581ec4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffce97ff7f1d6a2641d761aad6457883babe333075a454b5860d266719bdb3f9 -size 68525 +oid sha256:e9a561ca3aa61371cc1bd0c1b51508469a0343de3bd8f404055041591c60d42f +size 68231 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png index ec221ef43a..7c25276223 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58718344783197c680e18a7a3c97a3531293b6e2442ca5701d4605ecb567e70a -size 79724 +oid sha256:c739add59dd40303652319c0b789ec4d0d2224cc097598ef717e8fb101031ad7 +size 79471 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png index 8369183006..a3fcdfc5f0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d84207a6b1bf4815d2515974c721a0ba26fb406a31e96f3566b90e548c7bf49 -size 57077 +oid sha256:61d30aca9b213938462659c2dcb8bc8ef10b28821f8492715003262f4cae142c +size 56761 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png index 7cd4c1a580..e10bdbdf26 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2056d90bbf2676fae7d88b326c496aae95a1c31924c8b5d13ca363f486c72968 -size 56206 +oid sha256:a84be2101e96ab4e7566e4fe1af14243466c7dba7e540fb7502acbbf70d33aaa +size 55835 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png index 6b23cf2db8..29ac8cd6f3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bca7347130b0b449ef2c917359753660446e4be7d2d4a13323489c63dbf28a4 -size 63589 +oid sha256:9f168b3af0e3679a2d7a5f2baddc1523c1afe9dc433d25c267572097d9ab7d15 +size 63284 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png index f388e96dac..f405aa34cb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fffdb6a8f159af3104d9f2e8edbc11395854f6ac6a558e303d20c79ea91857b -size 98128 +oid sha256:41799e15fad757a976b278d5d722fc755dcfdda3ad0880a1f4058a337b96d470 +size 97907 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png index 37890657c9..9b72a8c4fd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534628991ee750aa4f4b4e3edda1ea26cf648eb74d06610d4207266b659d4421 -size 55083 +oid sha256:c3d27473a9375edc590aeedf8dcb188e1c23785a8f8c62900aa439c33f86ca13 +size 54772 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png index cb402d830b..7a87bb30d2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9de6ef77a1d574da317c3d1c3258f3068e79613387d0bc32e925f516338da9e9 -size 55092 +oid sha256:52a125268370bb369c53271f204b5f942da75f01639a39ae6b935d3f3ab4ef00 +size 54705 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png index 06a6ee8488..d0e908caff 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:350c89c3491e08a80f9910121140d56f2cf28fdc0b8f6ced380d1ec34b116c52 -size 63975 +oid sha256:56d531df665ae3baa6400b0fc83b1fe756ec0f452ddd2423457ed26ea18aed98 +size 63628 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png index e698f48771..29df858167 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8659bbb89e5c26caea7f1d759e1c9bbd6ecedc525dcc2cb6b1cc844c835aeec1 -size 54455 +oid sha256:d4e66a9e00b7aa05294522055cf6a89e68d409496c1a24ef0b6d841221f6883b +size 54122 diff --git a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/services.apperror.impl_AppErrorView_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/services.apperror.api_AppErrorView_Night_0_en.png diff --git a/tools/localazy/checkForbiddenTerms.py b/tools/localazy/checkForbiddenTerms.py index e190fcea68..123246ffd0 100755 --- a/tools/localazy/checkForbiddenTerms.py +++ b/tools/localazy/checkForbiddenTerms.py @@ -31,6 +31,9 @@ forbiddenTerms = { # We explicitly want to mention Element Pro in these 2: "screen_change_server_error_element_pro_required_title", "screen_change_server_error_element_pro_required_message", + # Contains "Element Classic" + "screen_missing_key_backup_open_element_classic", + "screen_missing_key_backup_step_1", ] } diff --git a/tools/localazy/config.json b/tools/localazy/config.json index d3e44b7c08..b38268e5f7 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -170,6 +170,8 @@ "name" : ":features:login:impl", "includeRegex" : [ "screen_onboarding_.*", + "screen\\.onboarding\\..*", + "screen\\.missing_key_backup\\..*", "screen_login_.*", "screen_server_confirmation_.*", "screen_change_server_.*", diff --git a/tools/sdk/build-rust-sdk b/tools/sdk/build-rust-sdk index b3b57f7e98..2012af8741 100755 --- a/tools/sdk/build-rust-sdk +++ b/tools/sdk/build-rust-sdk @@ -41,7 +41,7 @@ sdkArg="" ## Argument parsing -TEMP=$(getopt -o 'rs:b:at:h' --long 'remote,sdk:,branch:,build-app,target-arch,help' -- "$@") +TEMP=$(getopt -o 'rs:b:at:h' --long 'remote,sdk:,branch:,build-app,target-arch:,help' -- "$@") if [ $? -ne 0 ]; then echo 'Terminating...' >&2