diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 95f322a2e4..9ed1919e0e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -52,6 +52,7 @@ Uncomment this markdown table below and edit the last line `|||`: +- [ ] I am aware of the [etiquette](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#etiquette). - This PR was made with the help of AI: - [ ] Yes. In this case, please request a review by Copilot. - [ ] No. diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 17e1fa0f1c..c16310cf43 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -34,6 +34,13 @@ "/^org.jetbrains.kotlinx:kotlinx-datetime/", ], }, + { + // Keep Guava on the Android variant and ignore jre-only upgrades. + "matchPackageNames": [ + "com.google.guava:guava", + ], + "allowedVersions": "/-android$/", + }, { // Limit PostHog Android upgrade to one PR per month, the first day of the month "matchPackageNames": [ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd5524c7e3..99b5ce9ef0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,19 +44,19 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APKs @@ -80,7 +80,7 @@ jobs: path: | app/build/outputs/apk/gplay/debug/*-universal-debug.apk app/build/outputs/apk/fdroid/debug/*-universal-debug.apk - - uses: rnkdsh/action-upload-diawi@4e1421305be7cfc510d05f47850262eeaf345108 # v1.5.12 + - uses: rnkdsh/action-upload-diawi@76778fcd1f9f589efaaa62f63d5f03430bbb1306 # v1.5.13 id: diawi # Do not fail the whole build if Diawi upload fails continue-on-error: true diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index 956655b262..c176864b46 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -43,7 +43,7 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -56,12 +56,12 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug Gplay Enterprise APK diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 4bb51d05b5..a8bf7b623a 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ jobs: # Skip in forks, it doesn't work even with the fallback token if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Add SSH private keys for submodule repositories @@ -24,7 +24,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5 + uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index 55a300dd88..4b0d28b00b 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -18,12 +18,12 @@ jobs: - name: ⏬ Checkout with LFS uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 @@ -36,7 +36,7 @@ jobs: mkdir -p screenshots/en cp tests/uitests/src/test/snapshots/images/* screenshots/en - name: Deploy GitHub Pages - uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./screenshots diff --git a/.github/workflows/gradle-wrapper-update.yml b/.github/workflows/gradle-wrapper-update.yml index 66078b7b4b..d29cca5400 100644 --- a/.github/workflows/gradle-wrapper-update.yml +++ b/.github/workflows/gradle-wrapper-update.yml @@ -13,10 +13,10 @@ jobs: # Skip in forks if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 name: Use JDK 21 if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' with: diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index 9af5c6bc1d..558d1b0a0c 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -38,19 +38,19 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.ref }} persist-credentials: false - - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 name: Use JDK 21 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK @@ -78,7 +78,7 @@ jobs: concurrency: group: maestro-test steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch' with: # Ensure we are building the branch and not the branch after being merged on develop diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 31a8806a85..f82d904a89 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,11 +32,11 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 371c11b3ff..54f77ea35d 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -37,13 +37,13 @@ jobs: uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: false @@ -76,16 +76,16 @@ jobs: name: Dependency analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c8c0a98dea..e24847280e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Check membership if: github.event.pull_request.user.login != 'renovate[bot]' - uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3 + uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2 id: teams with: username: ${{ github.event.pull_request.user.login }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ceaa86016a..c2bf89a07f 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -33,7 +33,7 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Add SSH private keys for submodule repositories @@ -51,7 +51,7 @@ jobs: name: Search for invalid screenshot files runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python 3.12 @@ -65,16 +65,16 @@ jobs: name: Search for invalid dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 @@ -93,7 +93,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -108,12 +108,12 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Konsist tests @@ -134,7 +134,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -149,12 +149,12 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run compose tests @@ -168,7 +168,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -183,12 +183,12 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Build Gplay Debug @@ -213,7 +213,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -228,12 +228,12 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Detekt @@ -254,7 +254,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -269,12 +269,12 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run Ktlint check @@ -295,7 +295,7 @@ jobs: group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -319,7 +319,7 @@ jobs: name: Check shell scripts runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Run shellcheck @@ -333,10 +333,10 @@ jobs: permissions: security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 upload_reports: name: Project Check Suite @@ -344,7 +344,7 @@ jobs: needs: [konsist, lint, ktlint, detekt] if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 @@ -360,7 +360,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5 + uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 0f4c8ee581..7465a6d6f8 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -17,6 +17,7 @@ jobs: permissions: # Need write permissions on PRs to remove the label "Record-Screenshots" pull-requests: write + contents: write name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }} runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots' @@ -53,13 +54,13 @@ jobs: with: persist-credentials: false - name: ☕️ Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.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 eece5ab0d4..fee9efb518 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,16 +34,16 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} @@ -71,7 +71,7 @@ jobs: group: ${{ format('build-release-main-enterprise-{0}', github.sha) }} cancel-in-progress: true steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Add SSH private keys for submodule repositories @@ -82,12 +82,12 @@ jobs: - name: Clone submodules run: git submodule update --init --recursive - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - name: Create Enterprise app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} @@ -122,16 +122,16 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 - name: Create APKs env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 40cf9d0058..4cb6bddf39 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -38,19 +38,19 @@ jobs: docker-images: true swap-storage: false - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Build debug code and test fixtures diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 1958e80083..622fecc403 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -12,7 +12,7 @@ jobs: permissions: issues: write steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: only-labels: "X-Needs-Info" days-before-issue-stale: 30 diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 6de0f3b0df..8d08702e4a 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -13,16 +13,16 @@ jobs: # Skip in forks if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Set up Python 3.12 diff --git a/.github/workflows/sync-sas-strings.yml b/.github/workflows/sync-sas-strings.yml index 9f7a67cc22..f84b1596cb 100644 --- a/.github/workflows/sync-sas-strings.yml +++ b/.github/workflows/sync-sas-strings.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} # No concurrency required, runs every time on a schedule. steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Set up Python 3.12 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 87551c4360..5466bc80af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,12 +63,12 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} run: git submodule update --init --recursive - name: ☕️ Use JDK 21 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '21' - name: Configure gradle - uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 + uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.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@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 8e8d03c9c4..b93ea81403 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -10,7 +10,7 @@ jobs: triage-new-issues: runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 with: project-url: https://github.com/orgs/element-hq/projects/91 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index 3ec20f332b..0b587369ed 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -14,7 +14,7 @@ jobs: if: > github.repository == 'element-hq/element-x-android' steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 with: project-url: https://github.com/orgs/element-hq/projects/43 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -23,7 +23,7 @@ jobs: name: Move triaged needs info issues on board runs-on: ubuntu-latest steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 id: addItem with: project-url: https://github.com/orgs/element-hq/projects/91 @@ -47,7 +47,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Element X Feature') steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 with: project-url: https://github.com/orgs/element-hq/projects/73 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -58,7 +58,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'Team: Verticals Feature') steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 with: project-url: https://github.com/orgs/element-hq/projects/57 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -70,7 +70,7 @@ jobs: contains(github.event.issue.labels.*.name, 'Team: QA') || contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 with: project-url: https://github.com/orgs/element-hq/projects/69 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -81,7 +81,7 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'X-Needs-Signoff') steps: - - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2 with: project-url: https://github.com/orgs/element-hq/projects/89 github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 76f6344777..089816d5c8 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,7 @@ - diff --git a/.maestro/tests/roomList/createAndDeleteDM.yaml b/.maestro/tests/roomList/createAndDeleteDM.yaml index a6279151ea..eed576c04f 100644 --- a/.maestro/tests/roomList/createAndDeleteDM.yaml +++ b/.maestro/tests/roomList/createAndDeleteDM.yaml @@ -7,7 +7,7 @@ appId: ${MAESTRO_APP_ID} - tapOn: text: ${MAESTRO_INVITEE1_MXID} index: 1 -- tapOn: "Send invite" +- tapOn: "Continue" - takeScreenshot: build/maestro/330-createAndDeleteDM - tapOn: "maestroelement2" - scroll diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml index b53066ccd5..adf9d7cf29 100644 --- a/.maestro/tests/roomList/createAndDeleteRoom.yaml +++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml @@ -24,8 +24,16 @@ appId: ${MAESTRO_APP_ID} text: ${MAESTRO_INVITEE2_MXID} index: 1 - tapOn: "Invite" +- runFlow: + when: + visible: 'Invite new contact to this room?' + commands: + - tapOn: + id: "confirm_invite_unknown" +# Close the keyboard if it's still open +- tapOn: "Back" +# Go back to the room details screen - tapOn: "Back" -- tapOn: "aRoomName" - scrollUntilVisible: direction: DOWN element: diff --git a/AGENTS.md b/AGENTS.md index 454c347a9f..d206be98c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,11 +44,14 @@ PRs must meet these rules. ## Pull Request Guidelines -- Use sentence-style commit/PR messages (no conventional commits). -- Apply exactly **one** `PR-` label for changelog categorization. -- PR title = changelog entry — make it descriptive; no "Fixes #…" prefixes. -- Include screenshots or screen recordings for any UI changes. -- Keep PRs focused; split changes over 1000 lines. +- Sentence-style titles (no conventional commits). +- Exactly one `pr-` label (see `.github/release.yml`). +- Title = changelog entry — descriptive, no "Fixes #…". +- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette). +- Screenshots/videos for visual changes. +- 500 additions max — split large changes. +- Commits need a title and description; no tiny or massive commits. +- No history rewrites. --- diff --git a/CHANGES.md b/CHANGES.md index df109d34a6..f08a3f9d13 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,365 @@ +Changes in Element X v26.06.4 +============================= + + + +## What's Changed +### ✨ Features +* Add well-known parsing and extension seam for custom recovery passphrase by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6944 +### 🙌 Improvements +* Add ability to forward a message to several rooms. by @bmarty in https://github.com/element-hq/element-x-android/pull/7013 +### 🐛 Bugfixes +* `screen_custom_recovery_key_input_notice` is now a plurals. by @bmarty in https://github.com/element-hq/element-x-android/pull/7045 +* Replace `ContentResolver.delete` usage with a backwards-compatible version by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7053 +### Dependency upgrades +* Update kotlin to v0.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7036 +* Update gradle/actions action to v6.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7037 +* Update jna monorepo to v5.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7026 +* Update coil to v3.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7043 +* Update metro to v1.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7048 +* Update dependency io.sentry:sentry-android to v8.43.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7047 +* Update dependencyAnalysis to v3.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7050 +* Compile with sdk37 and target API 37 by @bmarty in https://github.com/element-hq/element-x-android/pull/7041 +### Others +* call: Remove deprecated web<->EX api calls, use the new ones by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6628 +* Fix deprecation warning by @bmarty in https://github.com/element-hq/element-x-android/pull/7042 +* Let Divider use our theme color. by @bmarty in https://github.com/element-hq/element-x-android/pull/7054 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.06.3...v26.06.4 + +Changes in Element X v26.06.3 +============================= + + + +## What's Changed +### ✨ Features +* Click on avatar in moderation bottom sheet opens avatar preview by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6991 +### 🙌 Improvements +* Change: hide UserLocationPuck when sharing from current device by @ganfra in https://github.com/element-hq/element-x-android/pull/6955 +* Replace "Edit" button with Crop icon. by @bmarty in https://github.com/element-hq/element-x-android/pull/6953 +* change(location): ensure permissions are always requested at least once by @ganfra in https://github.com/element-hq/element-x-android/pull/6996 +* Refactor: replace client-side event filtering for public rooms with SDK by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7025 +### 🐛 Bugfixes +* Ensure the video preview can be played several times. by @bmarty in https://github.com/element-hq/element-x-android/pull/6964 +* Fix "Sent" checkmark disappearing when the message is read and "Share presence" is disabled by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6985 +* Make `BaseRoom.getDirectRoomMember` change when `roomInfo.isDm` changes by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6989 +* Fix invites not being sent during the room creation by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7018 +* Fix sharing media inside the app by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7016 +* Fix ANRs when calculating DB sizes for Sentry by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7017 +* Fix thread list not loading older threads by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7034 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6986 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7021 +### 🧱 Build +* Try fixing flaky screenshots for location timeline items by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6954 +* `ScanQrCodeViewTest`: try fixing broken test in CI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7027 +### 🚧 In development 🚧 +* Ensure that the application rotate the QrCode if one day the SDK emits the `.Expired` error. by @bmarty in https://github.com/element-hq/element-x-android/pull/6993 +### Dependency upgrades +* Update dependency androidx.javascriptengine:javascriptengine to v1.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6785 +* Update dependency com.google.firebase:firebase-bom to v34.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6967 +* Update dependency com.github.matrix-org:matrix-analytics-events to v0.36.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6963 +* Update dependencyAnalysis to v3.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6892 +* Update dependency io.element.android:element-call-embedded to v0.20.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6988 +* Update dependency io.mockk:mockk to v1.14.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6977 +* Update zizmorcore/zizmor-action action to v0.5.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6845 +* deps(codecov) : bumps to v7.0.0 to unblock CI by @ganfra in https://github.com/element-hq/element-x-android/pull/7002 +* Update wysiwyg to v2.42.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7009 +* Update actions/checkout action to v6.0.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6997 +* fix(deps): update android.gradle.plugin to v9 (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6021 +* Update dependencyAnalysis to v3.14.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6995 +* Update dependency io.sentry:sentry-android to v8.43.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6895 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7014 +* Update dependency org.maplibre.gl:android-sdk to v13.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7022 +* Update dependency com.google.firebase:firebase-bom to v34.14.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7029 +* Update okhttp monorepo to v5.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7031 +### Others +* Fix some string mappings for link new device errors by @hughns in https://github.com/element-hq/element-x-android/pull/6675 +* Fallback to the default push gateway on error by @p1gp1g in https://github.com/element-hq/element-x-android/pull/5742 +* Fix some string mappings for link new device errors by @bmarty in https://github.com/element-hq/element-x-android/pull/6958 +* Add `/myroomnick` slash command to change your display name in the current room by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6865 +* Update release notes for `v26.05.1` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6990 +* Fix message drafts not being saved in Threads by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6984 +* build-rust-sdk: allow skipping git update for kotlin components by @richvdh in https://github.com/element-hq/element-x-android/pull/7012 +* Fix broken screenshot tests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7038 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.06.2...v26.06.3 + +Changes in Element X v26.06.2 +============================= + +Security fixes 🔐 +----------------- +- Fixed an issue with OIDC callback URLs that could cause the app to crash [(GHSA-r4f9-46vw-v7g3)](https://github.com/element-hq/element-x-android/security/advisories/GHSA-r4f9-46vw-v7g3). + +Changes in Element X v26.06.1 +============================= + +The release of `v26.06.0` was cancelled because it contained some changes that would prevent some users with devices having no Vulkan support from being able to install the app. This release contains the same changes as `v26.06.0` but with the Vulkan requirement removed. + +## What's Changed +### ✨ Features +* Image edition before sending: crop and rotate. by @bmarty in https://github.com/element-hq/element-x-android/pull/6842 +* Use a raw key for the SDK stores for new sessions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6835 +* Notification settings: pick custom sounds for messages and call ringtones by @bmarty in https://github.com/element-hq/element-x-android/pull/6897 +### 🙌 Improvements +* Do not show membership/profile events in public rooms by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6360 +* Use `runBlocking` for the token refresh logic by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6863 +* Move empty day separator filtering to a timeline post-processor by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6866 +* Add better logs to track token update failures by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6859 +* Read map tiler custom style from matrix .well-known file by @bmarty in https://github.com/element-hq/element-x-android/pull/6886 +* change(permissions) : allow to change ShareLiveLocation permission by @ganfra in https://github.com/element-hq/element-x-android/pull/6890 +* Avoid SQLCipher key derivation by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6774 +* change: replace the maplibre-compose UserLocationState to a simpler one by @ganfra in https://github.com/element-hq/element-x-android/pull/6913 +* Change : location provider by @ganfra in https://github.com/element-hq/element-x-android/pull/6935 +* Add flip actions to image edition by @bmarty in https://github.com/element-hq/element-x-android/pull/6949 +* Remove the `FloatingDateBadge` feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6950 +### 🐛 Bugfixes +* Fix media viewer flickering by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6800 +* Fix 'Conversation label cannot be empty' error by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6823 +* Release proximity wakelock on Element Call when call ends by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6825 +* Hide edit pencil from accessibility by @bmarty in https://github.com/element-hq/element-x-android/pull/6826 +* Don't compress images sent through the Files attachment picker by @cizra in https://github.com/element-hq/element-x-android/pull/6755 +* [a11y] Improve accessibility of video and audio player by @bmarty in https://github.com/element-hq/element-x-android/pull/6830 +* [a11y] Improve accessibility of screen headers. by @bmarty in https://github.com/element-hq/element-x-android/pull/6827 +* Add mark as read / unread in room details by @bmarty in https://github.com/element-hq/element-x-android/pull/6818 +* Fix formatting inconsistencies in latest event summaries by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6855 +* Fix public read receipts being sent by mistake by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6838 +* Fix app having a pink top bar in the recent app list when PIN lock is setup by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6903 +* Create log messages from `WebView` just once by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6923 +* Use secondary color icon for leading icons by @bmarty in https://github.com/element-hq/element-x-android/pull/6926 +* Use reverse ordering for `FilterEmptyDayPostProcessor` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6927 +* Make Vulkan a *not required* feature by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6961 +### 🗣 Translations +* Sync Strings - new translations to Catalan by @ElementBot in https://github.com/element-hq/element-x-android/pull/6856 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6921 +### 🧱 Build +* Remove DI-generated code from Kover reports by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6834 +* Disable cron jobs and private SSH key jobs in forks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6821 +* Revert "Disable cron jobs and private SSH key jobs in forks" by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6905 +* Downgrade detekt compose rules by @bmarty in https://github.com/element-hq/element-x-android/pull/6846 +* Add Stefan's etiquette to the project. by @bmarty in https://github.com/element-hq/element-x-android/pull/6909 +### 🚧 In development 🚧 +* [Link new device] Rotate QrCode instead of showing an error by @bmarty in https://github.com/element-hq/element-x-android/pull/6817 +* Fix image resizing behavior. by @bmarty in https://github.com/element-hq/element-x-android/pull/6924 +### Dependency upgrades +* Update dependency net.zetetic:sqlcipher-android to v4.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6819 +* Update dependencyAnalysis to v3.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6813 +* Update metro to v1.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6832 +* Update dependency io.element.android:element-call-embedded to v0.19.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6833 +* Update kotlin to v2.3.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6822 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.20 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6831 +* Update media3 to v1.10.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6816 +* Update peaceiris/actions-gh-pages action to v4.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6820 +* Update dependencyAnalysis to v3.12.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6840 +* Update codecov/codecov-action action to v6.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6864 +* Update dependency io.element.android:element-call-embedded to v0.20.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6876 +* Update roborazzi to v1.62.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6858 +* Update dependency androidx.compose:compose-bom to v2026.05.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6888 +* Update roborazzi to v1.63.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6893 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.26 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6885 +* Update dependency org.maplibre.compose:maplibre-compose to v0.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6891 +* Update actions/stale action to v10.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6904 +* Update tspascoal/get-user-teams-membership action to v4.0.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6900 +* Update dependency org.maplibre.gl:android-sdk to v13.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6930 +* Update dependency org.unifiedpush.android:connector to v3.3.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6937 +* Update kotlin to v2.3.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6946 +* Update dependency com.posthog:posthog-android to v3.47.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6920 +* Import compound token from release 10.2.1 https://github.com/element-hq/compound-design-tokens/releases/tag/v10.2.1 by @bmarty in https://github.com/element-hq/element-x-android/pull/6942 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6947 +* Update dependency com.github.matrix-org:matrix-analytics-events to v0.36.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6939 +### Others +* Make the avatar in the room member moderation bottom sheet clickable by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6705 +* Do not hide your own media for "Show media in timeline" by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6898 +* Report problem: add an optional TextField to provide a GitHub issue number. by @bmarty in https://github.com/element-hq/element-x-android/pull/6911 +* Fix read receipts not shown for calls by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6889 +* Reorder room detail items by @bmarty in https://github.com/element-hq/element-x-android/pull/6929 +* [Media viewer] File preview improvement by @bmarty in https://github.com/element-hq/element-x-android/pull/6933 +* [Media bottom sheet] UI iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6938 +* Add unread count to the room unread indicator by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6887 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.05.2...v26.06.1 + +Changes in Element X v26.05.2 +============================= + + + +## What's Changed +### ✨ Features +* Remove SignInWithClassic FeatureFlag to enable the feature. by @bmarty in https://github.com/element-hq/element-x-android/pull/6698 +* Create a new room when inviting people in a DM by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6756 +* Remove LiveLocationSharing feature flag by @ganfra in https://github.com/element-hq/element-x-android/pull/6811 +### 🙌 Improvements +* Disable biometric unlock when we disable pin code unlock by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6781 +### 🐛 Bugfixes +* Fix room list duplicate-detection telemetry crashing before it can report by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6791 +* Only load full media on media viewer when it's the visible item by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6794 +* Attempt to fix room list item duplicates at midnight by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6793 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6798 +### 🧱 Build +* Fix Maestro again after changes to the invite flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6796 +* Renovate: Keep Guava on the Android variant and ignore jre-only upgrades by @bmarty in https://github.com/element-hq/element-x-android/pull/6776 +### Dependency upgrades +* Update dependency androidx.compose:compose-bom to v2026.05.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6784 +* Update dependency io.sentry:sentry-android to v8.41.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6787 +* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6790 +* Update camera to v1.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6783 +* Update dependency androidx.webkit:webkit to v1.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6786 +* Update dependency com.google.firebase:firebase-bom to v34.13.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6789 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6805 +### Others +* Add MIDI playback by @cizra in https://github.com/element-hq/element-x-android/pull/6770 +* Show error message when using "Sign in with QR code" with a QR from a device that is also not signed in by @hughns in https://github.com/element-hq/element-x-android/pull/6802 + +## New Contributors +* @jennaharris7 made their first contribution in https://github.com/element-hq/element-x-android/pull/6791 +* @cizra made their first contribution in https://github.com/element-hq/element-x-android/pull/6770 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.05.1...v26.05.2 + +Changes in Element X v26.05.1 +============================= + + + +Security fixes 🔐 +----------------- + +This release contains some important security fixes: + +- Check the user ID in the `sender_device_keys` property of Olm-encrypted to-device events to prevent sender spoofing by homeserver owners. ([#6553](https://github.com/matrix-org/matrix-rust-sdk/pull/6553), High, [CVE-2026-45056](https://www.cve.org/CVERecord?id=CVE-2026-45056), [GHSA-wfq4-36m3-9g42](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-wfq4-36m3-9g42)) +- Reject invalid edits as candidates for timeline updates. ([#6454](https://github.com/matrix-org/matrix-rust-sdk/pull/6454), Moderate, [CVE-2026-45057](https://www.cve.org/CVERecord?id=CVE-2026-45057), [GHSA-h97m-27fx-42rx](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-h97m-27fx-42rx)) + +## What's Changed +### ✨ Features +* Make Element Call screen work edge-to-edge by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6634 +### 🙌 Improvements +* Stop removing the `logs` dir when clearing cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6765 +* Adapt to new DM definition changes in the SDK by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6748 +* feat: Update call started timeline item + declined support by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6649 +### 🐛 Bugfixes +* Improve pin code UX by @bmarty in https://github.com/element-hq/element-x-android/pull/6744 +* Use just the other user's avatar for DM details by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6738 +* Improve `FetchPushForegroundService`'s reliability by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6757 +* Prevent user from starting Live Location Sharing in thread by @bmarty in https://github.com/element-hq/element-x-android/pull/6767 +* Fix media playback from the timeline broken when exiting a thread by @bmarty in https://github.com/element-hq/element-x-android/pull/6771 +* Pin code: remove the key if there is no pin code by @bmarty in https://github.com/element-hq/element-x-android/pull/6780 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6761 +### 🚧 In development 🚧 +* Feature : share live location by @ganfra in https://github.com/element-hq/element-x-android/pull/6741 +### Dependency upgrades +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6746 +* Update actions/add-to-project action to v2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6758 +* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6759 +* Update dependency io.element.android:element-call-embedded to v0.19.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6766 +* Update metro to v1 (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6720 +* Update tspascoal/get-user-teams-membership action to v4.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6750 +* Update plugin sonarqube to v7.3.0.8198 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6743 +* Update plugin dependencycheck to v12.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6760 +* Update dependency com.google.guava:guava to v33.6.0-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6646 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6779 +### Others +* Render media captions formatting in the media viewer by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6729 +* Reduce FeatureFlag `Knock` effect on room creation and room edition forms by @bmarty in https://github.com/element-hq/element-x-android/pull/6768 +* Use the right analytics span as a parent in `checkNetworkConnection` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6751 +* Add missing strings `theme.black` by @bmarty in https://github.com/element-hq/element-x-android/pull/6772 +* Map back button in web view to esc (revive fixed version of: https://github.com/element-hq/element-x-android/pull/6724) by @toger5 in https://github.com/element-hq/element-x-android/pull/6725 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.05.0...v26.05.1 + +Changes in Element X v26.05.0 +============================= + + + +## What's Changed +### ✨ Features +* Add flag for automatic back pagination feature by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6637 +* Promote "history sharing on invite" out of developer options by @richvdh in https://github.com/element-hq/element-x-android/pull/6647 +* Remove RoomDirectorySearch feature flag — always enable the feature by @Copilot in https://github.com/element-hq/element-x-android/pull/6736 +### 🙌 Improvements +* Change native back button behavior in EC view (close settings in EC with os native back) by @toger5 in https://github.com/element-hq/element-x-android/pull/6642 +* Revert PR #6642 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6724 +* Use 'Report a problem' string instead of 'Report bug' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6735 +### 🐛 Bugfixes +* Remove distributed tracing of the 'timeline loading' flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6644 +* Set max lines for 'in reply to' view conditionally by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6612 +* Mention pill cut off by @bmarty in https://github.com/element-hq/element-x-android/pull/6651 +* Ensure that bottom sheet can scroll by @bmarty in https://github.com/element-hq/element-x-android/pull/6661 +* Remove legacy `mx-reply` from `toPlainText` formatted event contents by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6683 +* Fix ANRs when receiving push notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6696 +* Mitigate a deadlock when loading room timelines by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6674 +* Fix calls on Huawei devices: skip addWebMessageListener on Chromium < 119 by @manfrommedan in https://github.com/element-hq/element-x-android/pull/6640 +* Allow cancelling room loading in Home screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6723 +* Let our Json parser accept comments and trailing comma. by @bmarty in https://github.com/element-hq/element-x-android/pull/6700 +* Fix low width image message by @krbns in https://github.com/element-hq/element-x-android/pull/6692 +* Make icons in the Chat screen top bar 16dp by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6733 +* Fix back button sometimes not working after exiting a thread by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6732 +* Make send event state UI easier to click by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6739 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6658 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/6716 +### 🧱 Build +* Fix record screenshots action permissions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6679 +* Fix dependency error by @bmarty in https://github.com/element-hq/element-x-android/pull/6697 +### 🚧 In development 🚧 +* [Link new device] Add missing screen to render digits that the user has to type on the other device by @bmarty in https://github.com/element-hq/element-x-android/pull/6680 +### Dependency upgrades +* Update dependency io.nlopez.compose.rules:detekt to v0.5.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6594 +* Update zizmorcore/zizmor-action action to v0.5.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6630 +* Update dependency io.sentry:sentry-android to v8.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6597 +* fix(deps): update camera to v1.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6514 +* Update dependency io.sentry:sentry-android to v8.39.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6648 +* Update dependency io.element.android:element-call-embedded to v0.19.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6662 +* Update dependencyAnalysis to v3.9.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6657 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.04.27 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6666 +* Update dependency io.sentry:sentry-android to v8.40.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6691 +* Update dependency org.jsoup:jsoup to v1.22.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6660 +* Update kotlin by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6687 +* Update dependency androidx.compose:compose-bom to v2026.04.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6693 +* Update dependency io.nlopez.compose.rules:detekt to v0.5.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6711 +* Update dependency com.posthog:posthog-android to v3.43.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6704 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6718 +* Update roborazzi to v1.60.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6722 +* Update dependency net.zetetic:sqlcipher-android to v4.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6727 +* Update dependency org.maplibre.gl:android-sdk to v13.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6731 +* Update dependency org.matrix.rustcomponents:sdk-android to v26.05.6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6734 +* Update dependencyAnalysis to v3.10.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6742 +* Update tspascoal/get-user-teams-membership action to v4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/6747 +### Others +* devx: fix build sdk script options for macos by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/6636 +* PR:Fix mention pill cut off by @krbns in https://github.com/element-hq/element-x-android/pull/6622 +* Update media viewer UI by @bmarty in https://github.com/element-hq/element-x-android/pull/6643 +* Strip formatting from media captions in room summary by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6670 +* Update error mappings for Link new device flow by @hughns in https://github.com/element-hq/element-x-android/pull/6677 +* Rename `OIDC` components and variables to `OAuth` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6686 +* [Link new device] Add missing error case "already signed in" by @bmarty in https://github.com/element-hq/element-x-android/pull/6688 +* Improve detection of completion for Link new device flow by @hughns in https://github.com/element-hq/element-x-android/pull/6681 +* Remove external call support by @bmarty in https://github.com/element-hq/element-x-android/pull/6668 +* [a11y] Fix a set of issues by @bmarty in https://github.com/element-hq/element-x-android/pull/6650 +* Add clipping to RoomSummaryRow by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6654 +* Fix media viewer flickering and crashing by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6715 +* Rename verification methods by @bmarty in https://github.com/element-hq/element-x-android/pull/6726 +* Add a way to tweak MAS url. by @bmarty in https://github.com/element-hq/element-x-android/pull/6682 +* Fix 2 x Crash the app in Developer Options - Update AppDeveloperSettingsView.kt by @escix in https://github.com/element-hq/element-x-android/pull/6708 +* Introduce UI sample by @bmarty in https://github.com/element-hq/element-x-android/pull/6740 + +## New Contributors +* @krbns made their first contribution in https://github.com/element-hq/element-x-android/pull/6622 +* @toger5 made their first contribution in https://github.com/element-hq/element-x-android/pull/6642 +* @manfrommedan made their first contribution in https://github.com/element-hq/element-x-android/pull/6640 +* @Copilot made their first contribution in https://github.com/element-hq/element-x-android/pull/6736 + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.04.4...v26.05.0 + Changes in Element X v26.04.4 ============================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0126d89a9c..ec131aa65f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,8 @@ * [I want to help translating Element](#i-want-to-help-translating-element) * [I want to fix a bug](#i-want-to-fix-a-bug) * [I want to add a new feature or enhancement](#i-want-to-add-a-new-feature-or-enhancement) -* [Developer onboarding](#developer-onboarding) + * [Etiquette](#etiquette) + * [Developer onboarding](#developer-onboarding) * [Submitting the PRs](#submitting-the-prs) * [Android Studio settings](#android-studio-settings) * [Compilation](#compilation) @@ -63,7 +64,27 @@ Once we know that you want to contribute and have confirmed that the new feature Only once all of the above is met should you open a PR with your proposed changes. -## Developer onboarding +### Etiquette + +* As stated above all significant changes should be communicated through an issue before opening a PR +* We are happy to receive contributions but features always require maintenance, so depending on the change we might not be willing to accept it +* We are also fine with AI led contributions within reasonable bounds + * You are completely responsible for the quality of the PR + * If the PR doesn't show minimal effort (code does not compile, code does not work as expected, etc.) on your part it will be closed + * Please write the description yourself, we don't have the bandwidth to read LLM essays. The code needs to speak for itself. +* We use git for version control and GitHub for reviews, so in order to make everybody's life easier please: + * Keep the existing pull request template + * Don't submit large PRs, especially if not previously talked about. Anything above 200 lines is large (excluding generated code e.g. tests, translations, mocks) + * Please don't open unfinished PRs and expect us to fill in the details + * If you would like our opinion/direction on unfinished code please link your branch or idea in the ticket + * Please limit the number of commits in a single PR. We are perfectly happy with splitting work across multiple sessions as long as they're logically independant and show promise of progress (ideally expressed through a ticket) + * Each and every commit should stand on its own, clearly explaining what it does and why +* Once a PR goes into review please don't rewrite the history unless agreed so with the reviewer. + * Tweaks and fixes following review can be directly committed (to be interactively rebased later) or as fixups + +*The reviewer's response time will generally match yours. Switching contexts is very hard so please act accordingly. You are responsible for making the reviewers job enjoyable!* + +### Developer onboarding For a detailed overview of the project, see [Developer Onboarding](./docs/_developer_onboarding.md). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a4ee1c8459..ecd5ed8e7b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,11 +24,11 @@ import extension.buildConfigFieldStr import extension.locales import extension.setupDependencyInjection import extension.testCommonDependencies +import org.sonarqube.gradle.SonarResolverTask import java.util.Locale plugins { id("io.element.android-compose-application") - alias(libs.plugins.kotlin.android) // When using precompiled plugins, we need to apply the firebase plugin like this id(libs.plugins.firebaseAppDistribution.get().pluginId) id("kotlin-parcelize") @@ -103,13 +103,13 @@ android { logger.warnInBox("Building ${defaultConfig.applicationId} ($baseAppName) [$buildType]") buildTypes { - val oidcRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" + val oAuthRedirectSchemeBase = BuildTimeConfig.METADATA_HOST_REVERSED ?: "io.element.android" getByName("debug") { resValue("string", "app_name", "$baseAppName dbg") resValue( "string", "login_redirect_scheme", - "$oidcRedirectSchemeBase.debug", + "$oAuthRedirectSchemeBase.debug", ) applicationIdSuffix = ".debug" signingConfig = signingConfigs.getByName("debug") @@ -120,7 +120,7 @@ android { resValue( "string", "login_redirect_scheme", - oidcRedirectSchemeBase, + oAuthRedirectSchemeBase, ) signingConfig = signingConfigs.getByName("debug") @@ -157,7 +157,7 @@ android { resValue( "string", "login_redirect_scheme", - "$oidcRedirectSchemeBase.nightly", + "$oAuthRedirectSchemeBase.nightly", ) matchingFallbacks += listOf("release") signingConfig = signingConfigs.getByName("nightly") @@ -189,6 +189,7 @@ android { buildFeatures { buildConfig = true + resValues = true } flavorDimensions += "store" productFlavors { @@ -245,6 +246,11 @@ androidComponents { configureLicensesTasks(reportingExtension) } +// Configure the SonarQube plugin to wait for the resource generation tasks to complete before running the analysis. +tasks.withType().configureEach { + dependsOn("generateGplayDebugResValues", "generateGplayDebugAndroidTestResValues") +} + setupDependencyInjection() dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6041fbb118..d63e18ec1a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -75,7 +75,7 @@ android:scheme="elementx" /> diff --git a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProvider.kt similarity index 82% rename from app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt rename to app/src/main/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProvider.kt index ad4f9a47b2..16db564aaf 100644 --- a/app/src/main/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProvider.kt +++ b/app/src/main/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProvider.kt @@ -10,14 +10,14 @@ package io.element.android.x.oidc import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider +import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.x.R @ContributesBinding(AppScope::class) -class DefaultOidcRedirectUrlProvider( +class DefaultOAuthRedirectUrlProvider( private val stringProvider: StringProvider, -) : OidcRedirectUrlProvider { +) : OAuthRedirectUrlProvider { override fun provide() = buildString { append(stringProvider.getString(R.string.login_redirect_scheme)) append(":/") diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index af159a0b11..2e610fc0c7 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -11,7 +11,14 @@ @color/splashscreen_bg_dark @drawable/transparent @style/Theme.ElementX + @color/splashscreen_bg_dark + @color/splashscreen_bg_dark + @color/splashscreen_bg_dark - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 4190fd84f2..26cb0a56bd 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -10,8 +10,14 @@ @color/splashscreen_bg_light @drawable/transparent @style/Theme.ElementX + @color/splashscreen_bg_light + @color/splashscreen_bg_light + @color/splashscreen_bg_light diff --git a/app/src/main/res/xml/file_providers.xml b/app/src/main/res/xml/file_providers.xml index ac74590cba..330a2e0c3f 100644 --- a/app/src/main/res/xml/file_providers.xml +++ b/app/src/main/res/xml/file_providers.xml @@ -7,4 +7,5 @@ --> + diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index c95b3a5cc0..a77f42817d 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -2,6 +2,7 @@ + diff --git a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt index 4598a74e51..be059ed2a8 100644 --- a/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt +++ b/app/src/test/kotlin/io/element/android/x/intent/DefaultIntentProviderTest.kt @@ -24,14 +24,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.x.MainActivity import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -@RunWith(RobolectricTestRunner::class) -class DefaultIntentProviderTest { +class DefaultIntentProviderTest : RobolectricTest() { @Test fun `test getViewRoomIntent with data`() { val deepLinkCreator = lambdaRecorder { _, _, _, _ -> "deepLinkCreatorResult" } diff --git a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProviderTest.kt similarity index 89% rename from app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt rename to app/src/test/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProviderTest.kt index 18567355d2..c26e3dc692 100644 --- a/app/src/test/kotlin/io/element/android/x/oidc/DefaultOidcRedirectUrlProviderTest.kt +++ b/app/src/test/kotlin/io/element/android/x/oidc/DefaultOAuthRedirectUrlProviderTest.kt @@ -13,13 +13,13 @@ import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.x.R import org.junit.Test -class DefaultOidcRedirectUrlProviderTest { +class DefaultOAuthRedirectUrlProviderTest { @Test fun `test provide`() { val stringProvider = FakeStringProvider( defaultResult = "str" ) - val sut = DefaultOidcRedirectUrlProvider( + val sut = DefaultOAuthRedirectUrlProvider( stringProvider = stringProvider, ) val result = sut.provide() diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 45496acb77..64b9b76a14 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -48,6 +48,8 @@ android { } dependencies { + implementation(libs.coroutines.core) implementation(libs.androidx.annotationjvm) + implementation(libs.androidx.corektx) implementation(projects.libraries.matrix.api) } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 24a0355b3f..7440ecd2bf 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -33,13 +33,14 @@ dependencies { implementation(projects.libraries.deeplink.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.oidc.api) + implementation(projects.libraries.oauth.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.features.login.api) @@ -59,7 +60,7 @@ dependencies { testImplementation(projects.features.login.test) testImplementation(projects.features.share.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.oidc.test) + testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.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 70376ffcb9..14230d7c5a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -54,6 +54,7 @@ import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint +import io.element.android.features.location.api.live.ActiveLiveLocationShareManager import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer @@ -151,6 +152,7 @@ class LoggedInFlowNode( private val analyticsService: AnalyticsService, private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher, private val createRoomEntryPoint: CreateRoomEntryPoint, + private val activeLiveLocationShareManager: ActiveLiveLocationShareManager, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Placeholder, @@ -211,6 +213,7 @@ class LoggedInFlowNode( super.onBuilt() lifecycleScope.launch { sessionEnterpriseService.init() + activeLiveLocationShareManager.setup() } lifecycle.subscribe( onCreate = { @@ -219,7 +222,6 @@ class LoggedInFlowNode( loggedInFlowProcessor.observeEvents(sessionCoroutineScope) matrixClient.sessionVerificationService.setListener(verificationListener) mediaPreviewConfigMigration() - sessionCoroutineScope.launch { // Wait for the network to be connected before pre-fetching the max file upload size networkMonitor.connectivity.first { networkStatus -> networkStatus == NetworkStatus.Connected } @@ -380,9 +382,13 @@ class LoggedInFlowNode( } is NavTarget.Room -> { val joinedRoomCallback = object : JoinedRoomLoadedFlowNode.Callback { - override fun navigateToRoom(roomId: RoomId, serverNames: List) { + override fun onDone() { + backstack.pop() + } + + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) { lifecycleScope.launch { - attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = false) + attachRoom(roomIdOrAlias = roomId.toRoomIdOrAlias(), serverNames = serverNames, clearBackstack = clearBackStack) } } 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 0e458d3b9c..acf7b66db9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -63,8 +63,8 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.ui.common.nodes.emptyNode @@ -95,7 +95,7 @@ class RootFlowNode( private val signedOutEntryPoint: SignedOutEntryPoint, private val accountSelectEntryPoint: AccountSelectEntryPoint, private val intentResolver: IntentResolver, - private val oidcActionFlow: OidcActionFlow, + private val oAuthActionFlow: OAuthActionFlow, private val featureFlagService: FeatureFlagService, private val announcementService: AnnouncementService, private val analyticsService: AnalyticsService, @@ -392,7 +392,7 @@ class RootFlowNode( navigateTo(resolvedIntent.deeplinkData) } is ResolvedIntent.Login -> onLoginLink(resolvedIntent.params) - is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) + is ResolvedIntent.OAuth -> onOAuthAction(resolvedIntent.oAuthAction) is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData) is ResolvedIntent.IncomingShare -> onIncomingShare(resolvedIntent.shareIntentData) } @@ -529,8 +529,8 @@ class RootFlowNode( } } - private fun onOidcAction(oidcAction: OidcAction) { - oidcActionFlow.post(oidcAction) + private fun onOAuthAction(oAuthAction: OAuthAction) { + oAuthActionFlow.post(oAuthAction) } private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt index 9b1bbd1b81..1ce423a569 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt @@ -89,16 +89,14 @@ class SyncOrchestrator( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun observeStates() = coroutineScope.launch { Timber.tag(tag).d("start observing the app and network state") - - val isAppActiveFlow = combine( + val isAppActiveFlows = listOf( appForegroundStateService.isInForeground, appForegroundStateService.isInCall, appForegroundStateService.isSyncingNotificationEvent, appForegroundStateService.hasRingingCall, - ) { isInForeground, isInCall, isSyncingNotificationEvent, hasRingingCall -> - isInForeground || isInCall || isSyncingNotificationEvent || hasRingingCall - } - + appForegroundStateService.isSharingLiveLocation + ) + val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } } combine( // small debounce to avoid spamming startSync when the state is changing quickly in case of error. syncService.syncState.debounce(100.milliseconds), diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt index 6844db3ed6..ee316f00aa 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt @@ -18,13 +18,13 @@ import io.element.android.libraries.deeplink.api.DeeplinkData import io.element.android.libraries.deeplink.api.DeeplinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcIntentResolver +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthIntentResolver import timber.log.Timber sealed interface ResolvedIntent { data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent - data class Oidc(val oidcAction: OidcAction) : ResolvedIntent + data class OAuth(val oAuthAction: OAuthAction) : ResolvedIntent data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent data class Login(val params: LoginParams) : ResolvedIntent data class IncomingShare(val shareIntentData: ShareIntentData) : ResolvedIntent @@ -34,7 +34,7 @@ sealed interface ResolvedIntent { class IntentResolver( private val deeplinkParser: DeeplinkParser, private val loginIntentResolver: LoginIntentResolver, - private val oidcIntentResolver: OidcIntentResolver, + private val oAuthIntentResolver: OAuthIntentResolver, private val permalinkParser: PermalinkParser, private val shareIntentHandler: ShareIntentHandler, ) { @@ -45,9 +45,9 @@ class IntentResolver( val deepLinkData = deeplinkParser.getFromIntent(intent) if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData) - // Coming during login using Oidc? - val oidcAction = oidcIntentResolver.resolve(intent) - if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) + // Coming during login using OAuth? + val oAuthAction = oAuthIntentResolver.resolve(intent) + if (oAuthAction != null) return ResolvedIntent.OAuth(oAuthAction) val actionViewData = intent .takeIf { it.action == Intent.ACTION_VIEW } 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 febd15e9c2..dbe53b75ac 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 @@ -82,7 +82,8 @@ class JoinedRoomLoadedFlowNode( plugins = plugins, ), DependencyInjectionGraphOwner { interface Callback : Plugin { - fun navigateToRoom(roomId: RoomId, serverNames: List) + fun onDone() + fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean = false) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun navigateToGlobalNotificationSettings() fun navigateToDeveloperSettings() @@ -142,6 +143,10 @@ class JoinedRoomLoadedFlowNode( private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node { val callback = object : RoomDetailsEntryPoint.Callback { + override fun onDone() { + callback.onDone() + } + override fun navigateToGlobalNotificationSettings() { callback.navigateToGlobalNotificationSettings() } @@ -150,7 +155,7 @@ class JoinedRoomLoadedFlowNode( callback.navigateToDeveloperSettings() } - override fun navigateToRoom(roomId: RoomId, serverNames: List) { + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) { callback.navigateToRoom(roomId, serverNames) } diff --git a/appnav/src/main/res/values-ca/translations.xml b/appnav/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..d251b3a6b1 --- /dev/null +++ b/appnav/src/main/res/values-ca/translations.xml @@ -0,0 +1,6 @@ + + + "Tanca sessió i actualitza" + "%1$s ja no admet el protocol antic. Tanca sessió i torna a entrar per continuar utilitzant l\'aplicació." + "El servidor utilitzat ja no admet el protocol antic. Tanca sessió i torna-la a iniciar per continuar utilitzant l\'aplicació." + 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 6dffda9e42..5ef3d8cd70 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinedRoomLoadedFlowNodeTest.kt @@ -45,15 +45,13 @@ import io.element.android.services.analytics.test.watchers.FakeAnalyticsSendMess import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.test.FakeActiveRoomsHolder import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class JoinedRoomLoadedFlowNodeTest { +class JoinedRoomLoadedFlowNodeTest : RobolectricTest() { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt index 576e1aaea6..841f79678b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt @@ -26,16 +26,14 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.test.FakeOidcIntentResolver +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -@RunWith(RobolectricTestRunner::class) -class IntentResolverTest { +class IntentResolverTest : RobolectricTest() { @Test fun `resolve launcher intent should return null`() { val sut = createIntentResolver() @@ -170,9 +168,9 @@ class IntentResolverTest { } @Test - fun `test resolve oidc`() { + fun `test resolve OAuth`() { val sut = createIntentResolver( - oidcIntentResolverResult = { OidcAction.GoBack() }, + oAuthIntentResolverResult = { OAuthAction.GoBack() }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -180,8 +178,8 @@ class IntentResolverTest { } val result = sut.resolve(intent) assertThat(result).isEqualTo( - ResolvedIntent.Oidc( - oidcAction = OidcAction.GoBack() + ResolvedIntent.OAuth( + oAuthAction = OAuthAction.GoBack() ) ) } @@ -194,7 +192,7 @@ class IntentResolverTest { val sut = createIntentResolver( loginIntentResolverResult = { null }, permalinkParserResult = { permalinkData }, - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -213,7 +211,7 @@ class IntentResolverTest { val sut = createIntentResolver( permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, loginIntentResolverResult = { null }, - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -230,7 +228,7 @@ class IntentResolverTest { ) val sut = createIntentResolver( permalinkParserResult = { permalinkData }, - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_BATTERY_LOW @@ -244,7 +242,7 @@ class IntentResolverTest { fun `test incoming share simple`() { val shareIntentData = ShareIntentData.PlainText("Hello") val sut = createIntentResolver( - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, onIncomingShareIntent = { shareIntentData }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { @@ -260,7 +258,7 @@ class IntentResolverTest { val fileUri = "content://com.example.app/file1.jpg".toUri() val shareIntentData = ShareIntentData.Uris(text = "Hello", uris = listOf(UriToShare(fileUri, "image/jpg"))) val sut = createIntentResolver( - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, onIncomingShareIntent = { shareIntentData }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { @@ -277,7 +275,7 @@ class IntentResolverTest { val sut = createIntentResolver( permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }, loginIntentResolverResult = { null }, - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -292,7 +290,7 @@ class IntentResolverTest { val aLoginParams = LoginParams("accountProvider", null) val sut = createIntentResolver( loginIntentResolverResult = { aLoginParams }, - oidcIntentResolverResult = { null }, + oAuthIntentResolverResult = { null }, ) val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW @@ -306,7 +304,7 @@ class IntentResolverTest { deeplinkParserResult: DeeplinkData? = null, permalinkParserResult: (String) -> PermalinkData = { lambdaError() }, loginIntentResolverResult: (String) -> LoginParams? = { lambdaError() }, - oidcIntentResolverResult: (Intent) -> OidcAction? = { lambdaError() }, + oAuthIntentResolverResult: (Intent) -> OAuthAction? = { lambdaError() }, onIncomingShareIntent: (Intent) -> ShareIntentData? = { null }, ): IntentResolver { return IntentResolver( @@ -314,8 +312,8 @@ class IntentResolverTest { loginIntentResolver = FakeLoginIntentResolver( parseResult = loginIntentResolverResult, ), - oidcIntentResolver = FakeOidcIntentResolver( - resolveResult = oidcIntentResolverResult, + oAuthIntentResolver = FakeOAuthIntentResolver( + resolveResult = oAuthIntentResolverResult, ), permalinkParser = FakePermalinkParser( result = permalinkParserResult 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 902f446a6f..18c8cfd7b9 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 @@ -21,7 +21,7 @@ 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 import io.element.android.libraries.matrix.api.encryption.RecoveryState -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncState 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 2f17071870..ec36f3d32d 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 @@ -13,7 +13,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.tests.testutils.lambda.lambdaError class FakeJoinedRoomLoadedFlowNodeCallback : JoinedRoomLoadedFlowNode.Callback { - override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() + override fun onDone() = lambdaError() + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError() diff --git a/build.gradle.kts b/build.gradle.kts index dfd32f0872..0e0df9dbc6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,7 @@ tasks.register("clean").configure { delete(rootProject.layout.buildDirectory) } -private val ktLintVersion = the().versions.ktlint.get() +private val catalog = the() allprojects { // Detekt @@ -46,7 +46,7 @@ allprojects { config.from(files("$rootDir/tools/detekt/detekt.yml")) } dependencies { - detektPlugins("io.nlopez.compose.rules:detekt:0.5.7") + detektPlugins(catalog.detekt.compose.rules) detektPlugins(project(":tests:detekt-rules")) } @@ -64,7 +64,7 @@ allprojects { // See https://github.com/JLLeitschuh/ktlint-gradle#configuration configure { - version = ktLintVersion + version = catalog.versions.ktlint.get() android = true ignoreFailures = false enableExperimentalRules = true @@ -106,8 +106,6 @@ allprojects { // Fix compilation warning for annotations // See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details freeCompilerArgs.add("-Xannotation-default-target=first-only") - // Opt-in to context receivers - freeCompilerArgs.add("-Xcontext-parameters") } } } diff --git a/docs/oidc.md b/docs/oauth.md similarity index 81% rename from docs/oidc.md rename to docs/oauth.md index 23709b608c..1080c64b0e 100644 --- a/docs/oidc.md +++ b/docs/oauth.md @@ -1,4 +1,4 @@ -This file contains some rough notes about Oidc implementation, with some examples of actual data. +This file contains some rough notes about OAuth implementation, with some examples of actual data. [ios implementation](https://github.com/element-hq/element-x-ios/compare/develop...doug/oidc-temp) @@ -25,7 +25,7 @@ tosUri = "https://element.io/user-terms-of-service", policyUri = "https://element.io/privacy" -Example of OidcData (from presentUrl callback): +Example of OAuthData (from presentUrl callback): url: https://auth-oidc.lab.element.dev/authorize?response_type=code&client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&redirect_uri=io.element%3A%2Fcallback&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&state=ex6mNJVFZ5jn9wL8&nonce=NZ93DOyIGQd9exPQ&code_challenge_method=S256&code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&prompt=consent Formatted url: @@ -43,8 +43,8 @@ https://auth-oidc.lab.element.dev/authorize? state: ex6mNJVFZ5jn9wL8 -Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs -Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs +OAuth client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs +OAuth sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs Test server: diff --git a/enterprise b/enterprise index cdde60c158..efdf9d9a15 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463 +Subproject commit efdf9d9a15a5b600c55f674d64794e7737ad906e diff --git a/fastlane/metadata/android/en-US/changelogs/202605000.txt b/fastlane/metadata/android/en-US/changelogs/202605000.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202605000.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/202605010.txt b/fastlane/metadata/android/en-US/changelogs/202605010.txt new file mode 100644 index 0000000000..0ad08f5b4d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202605010.txt @@ -0,0 +1,2 @@ +Main changes in this version: improvements in Element Call, room knocking and room directory are now available, improvements on DMs. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/202605020.txt b/fastlane/metadata/android/en-US/changelogs/202605020.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202605020.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/202606000.txt b/fastlane/metadata/android/en-US/changelogs/202606000.txt new file mode 100644 index 0000000000..6c68999e99 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202606000.txt @@ -0,0 +1,2 @@ +Main changes in this version: added image editing before sending and an option for custom notification sounds, improved start times, several other 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/202606010.txt b/fastlane/metadata/android/en-US/changelogs/202606010.txt new file mode 100644 index 0000000000..08c06cf192 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202606010.txt @@ -0,0 +1,2 @@ +Main changes in this version: fixed an issue with devices not supporting Vulkan, added image editing before sending and an option for custom notification sounds, improved start times, several other 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/202606030.txt b/fastlane/metadata/android/en-US/changelogs/202606030.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202606030.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/202606040.txt b/fastlane/metadata/android/en-US/changelogs/202606040.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202606040.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-ca/translations.xml b/features/analytics/api/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..9f352d28ce --- /dev/null +++ b/features/analytics/api/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "Comparteix dades d\'ús anònimes per ajudar-nos a identificar problemes." + "Pots llegir tots els nostres termes %1$s." + "aquí" + "Comparteix dades analítiques" + diff --git a/features/analytics/api/src/main/res/values-ja/translations.xml b/features/analytics/api/src/main/res/values-ja/translations.xml index 5554ee162f..e1495271d3 100644 --- a/features/analytics/api/src/main/res/values-ja/translations.xml +++ b/features/analytics/api/src/main/res/values-ja/translations.xml @@ -1,7 +1,7 @@ - "問題発見のため、匿名の使用データの共有にご協力ください。" - "利用規約の全文を%1$sから確認することができます。" + "改善のため、匿名の使用データの共有にご協力ください。" + "規約の全文は%1$sから確認することができます。" "こちら" "使用データを共有" diff --git a/features/analytics/api/src/main/res/values-zh/translations.xml b/features/analytics/api/src/main/res/values-zh/translations.xml index e8f0fcb434..8f1c1699d9 100644 --- a/features/analytics/api/src/main/res/values-zh/translations.xml +++ b/features/analytics/api/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ "共享匿名使用数据以帮助我们排查问题。" - "你可以阅读我们的所有条款 %1$s。" + "你可以点击 %1$s 阅读我们的所有条款。" "此处" "共享分析数据" diff --git a/features/analytics/impl/src/main/res/values-ca/translations.xml b/features/analytics/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..5a2b633075 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,10 @@ + + + "No registrarem ni elaborarem perfils de cap dada personal" + "Comparteix dades d\'ús anònimes per ajudar-nos a identificar problemes." + "Pots llegir tots els nostres termes %1$s." + "aquí" + "Ho pots desactivar en qualsevol moment" + "No compartirem les teves dades amb tercers" + "Ajuda\'ns a millorar %1$s" + diff --git a/features/analytics/impl/src/main/res/values-ja/translations.xml b/features/analytics/impl/src/main/res/values-ja/translations.xml index 2cee69962c..162e01ecb0 100644 --- a/features/analytics/impl/src/main/res/values-ja/translations.xml +++ b/features/analytics/impl/src/main/res/values-ja/translations.xml @@ -1,8 +1,8 @@ "いかなる個人情報も記録, 分析されることはありません" - "問題発見のため、匿名の使用データの共有にご協力ください。" - "利用規約の全文を%1$sから確認することができます。" + "改善のため、匿名の使用データの共有にご協力ください。" + "規約の全文は%1$sから確認することができます。" "こちら" "いつでも設定は変更できます" "情報が第三者に共有されることはありません" diff --git a/features/analytics/impl/src/main/res/values-zh/translations.xml b/features/analytics/impl/src/main/res/values-zh/translations.xml index 8393e45e81..678d506287 100644 --- a/features/analytics/impl/src/main/res/values-zh/translations.xml +++ b/features/analytics/impl/src/main/res/values-zh/translations.xml @@ -2,7 +2,7 @@ "我们不会记录或分析任何个人数据" "共享匿名使用数据以帮助我们排查问题。" - "你可以阅读我们的所有条款 %1$s。" + "你可以点击 %1$s 阅读我们的所有条款。" "此处" "可以随时关闭此功能" "我们不会与第三方共享你的数据" 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 5a6c135247..2093baa8b3 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,8 +35,9 @@ 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) + // Default-status convention: Shown for one-shot intro/feature announcements that should + // NOT replay on first launch (Fullscreen.Space is disabled; NewNotificationSound is opted + // in by AppMigration08). val defaultStatus = when (announcement) { Announcement.Fullscreen.Space -> AnnouncementStatus.Shown Announcement.NewNotificationSound -> AnnouncementStatus.Shown diff --git a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt index b69037e61a..e049935212 100644 --- a/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt +++ b/features/announcement/impl/src/test/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.impl.AnnouncementEvent import io.element.android.features.announcement.impl.AnnouncementState @@ -20,43 +22,38 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class FullscreenAnnouncementViewTest { - @get:Rule val rule = createAndroidComposeRule() +class FullscreenAnnouncementViewTest : RobolectricTest() { @Test - fun `clicking on back sends a AnnouncementEvent`() { + fun `clicking on back sends a AnnouncementEvent`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setFullscreenAnnouncementView( + setFullscreenAnnouncementView( anAnnouncementState( announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } @Test - fun `clicking on Continue sends a AnnouncementEvent`() { + fun `clicking on Continue sends a AnnouncementEvent`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setFullscreenAnnouncementView( + setFullscreenAnnouncementView( anAnnouncementState( announcement = Announcement.Fullscreen.Space, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(AnnouncementEvent.Continue(Announcement.Fullscreen.Space)) } } -private fun AndroidComposeTestRule.setFullscreenAnnouncementView( +private fun AndroidComposeUiTest.setFullscreenAnnouncementView( state: AnnouncementState, ) { setContent { diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt similarity index 50% rename from features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt rename to features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt index 4b09813418..c1dcf573c6 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallType.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CallData.kt @@ -14,22 +14,9 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize -sealed interface CallType : NodeInputs, Parcelable { - @Parcelize - data class ExternalUrl(val url: String) : CallType { - override fun toString(): String { - return "ExternalUrl" - } - } - - @Parcelize - data class RoomCall( - val sessionId: SessionId, - val roomId: RoomId, - val isAudioCall: Boolean - ) : CallType { - override fun toString(): String { - return "RoomCall(sessionId=$sessionId, roomId=$roomId, isAudioCall=$isAudioCall)" - } - } -} +@Parcelize +data class CallData( + val sessionId: SessionId, + val roomId: RoomId, + val isAudioCall: Boolean +) : NodeInputs, Parcelable diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt index caa557f4de..2976635ee2 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt @@ -17,13 +17,13 @@ import io.element.android.libraries.matrix.api.core.UserId interface ElementCallEntryPoint { /** * Start a call of the given type. - * @param callType The type of call to start. + * @param callData The data of call to start. */ - fun startCall(callType: CallType) + fun startCall(callData: CallData) /** * Handle an incoming call. - * @param callType The type of call. + * @param callData The data of call. * @param eventId The event id of the event that started the call. * @param senderId The user id of the sender of the event that started the call. * @param roomName The name of the room the call is in. @@ -35,7 +35,7 @@ interface ElementCallEntryPoint { * @param textContent The text content of the notification. If null the default content from the system will be used. */ suspend fun handleIncomingCall( - callType: CallType.RoomCall, + callData: CallData, eventId: EventId, senderId: UserId, roomName: String?, diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 2843e47870..d01e31451e 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -84,7 +84,6 @@ dependencies { implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) implementation(libs.coil.compose) - implementation(libs.network.retrofit) implementation(libs.serialization.json) implementation(libs.element.call.embedded) api(projects.features.call.api) diff --git a/features/call/impl/src/main/AndroidManifest.xml b/features/call/impl/src/main/AndroidManifest.xml index daf1a910c9..c35c6843ff 100644 --- a/features/call/impl/src/main/AndroidManifest.xml +++ b/features/call/impl/src/main/AndroidManifest.xml @@ -30,44 +30,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:taskAffinity="io.element.android.features.call" /> (null) } - fun handleEvent(event: PictureInPictureEvents) { + fun handleEvent(event: PictureInPictureEvent) { when (event) { - is PictureInPictureEvents.SetPipController -> { + is PictureInPictureEvent.SetPipController -> { pipController = event.pipController } - PictureInPictureEvents.EnterPictureInPicture -> { + PictureInPictureEvent.EnterPictureInPicture -> { coroutineScope.launch { switchToPip(pipController) } } - is PictureInPictureEvents.OnPictureInPictureModeChanged -> { + is PictureInPictureEvent.OnPictureInPictureModeChanged -> { Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: ${event.isInPip}") isInPictureInPicture = event.isInPip if (event.isInPip) { diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt index b1fef4f28b..108589edb9 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureState.kt @@ -11,5 +11,5 @@ package io.element.android.features.call.impl.pip data class PictureInPictureState( val supportPip: Boolean, val isInPictureInPicture: Boolean, - val eventSink: (PictureInPictureEvents) -> Unit, + val eventSink: (PictureInPictureEvent) -> Unit, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt index 6324820eec..f4a78294b6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt @@ -11,7 +11,7 @@ package io.element.android.features.call.impl.pip fun aPictureInPictureState( supportPip: Boolean = false, isInPictureInPicture: Boolean = false, - eventSink: (PictureInPictureEvents) -> Unit = {}, + eventSink: (PictureInPictureEvent) -> Unit = {}, ): PictureInPictureState { return PictureInPictureState( supportPip = supportPip, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt index 179e6c2b22..bf27e8d39d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt @@ -13,7 +13,7 @@ import android.content.Context import android.content.Intent import androidx.core.content.IntentCompat import dev.zacsweers.metro.Inject -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.utils.ActiveCallManager @@ -42,7 +42,7 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() { context.bindings().inject(this) appCoroutineScope.launch { activeCallManager.hangUpCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = notificationData.sessionId, roomId = notificationData.roomId, isAudioCall = notificationData.audioOnly diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt index 3a51a014df..9e551b3e1b 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt @@ -9,6 +9,8 @@ package io.element.android.features.call.impl.ui import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.call.impl.notifications.CallNotificationData +import io.element.android.libraries.designsystem.preview.ROOM_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB 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 @@ -34,8 +36,8 @@ internal fun aCallNotificationData( roomId = RoomId("!1234:matrix.org"), eventId = EventId("\$asdadadsad:matrix.org"), senderId = UserId("@bob:matrix.org"), - roomName = "A room", - senderName = "Bob", + roomName = ROOM_NAME, + senderName = USER_NAME_BOB, avatarUrl = null, notificationChannelId = "incoming_call", timestamp = 0L, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.kt new file mode 100644 index 0000000000..cd47cd8bb1 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenBackPressPolicy.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.call.impl.ui +internal sealed interface CallScreenBackPressAction { + data object DispatchEscapeToWebView : CallScreenBackPressAction + data object EnterPictureInPicture : CallScreenBackPressAction +} + +internal object CallScreenBackPressPolicy { + fun resolve( + supportPip: Boolean, + hasWebView: Boolean, + fromNative: Boolean, + ): CallScreenBackPressAction? { + return when { + hasWebView && fromNative -> CallScreenBackPressAction.DispatchEscapeToWebView + hasWebView && supportPip -> CallScreenBackPressAction.EnterPictureInPicture + else -> null + } + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt similarity index 78% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt index 8fbbce896f..357559c3f9 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvents.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenEvent.kt @@ -10,8 +10,8 @@ package io.element.android.features.call.impl.ui import io.element.android.features.call.impl.utils.WidgetMessageInterceptor -sealed interface CallScreenEvents { - data object Hangup : CallScreenEvents - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents - data class OnWebViewError(val description: String?) : CallScreenEvents +sealed interface CallScreenEvent { + data object Hangup : CallScreenEvent + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvent + data class OnWebViewError(val description: String?) : CallScreenEvent } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index da2c57c0ac..7d8e20967f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -23,7 +23,7 @@ import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.compound.theme.ElementTheme -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.data.WidgetMessage import io.element.android.features.call.impl.utils.ActiveCallManager import io.element.android.features.call.impl.utils.CallWidgetProvider @@ -52,7 +52,7 @@ import kotlin.time.Duration.Companion.seconds @AssistedInject class CallScreenPresenter( - @Assisted private val callType: CallType, + @Assisted private val callData: CallData, @Assisted private val navigator: CallScreenNavigator, private val callWidgetProvider: CallWidgetProvider, userAgentProvider: UserAgentProvider, @@ -69,10 +69,9 @@ class CallScreenPresenter( ) : Presenter { @AssistedFactory interface Factory { - fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter + fun create(callData: CallData, navigator: CallScreenNavigator): CallScreenPresenter } - private val isInWidgetMode = callType is CallType.RoomCall private val userAgent = userAgentProvider.provide() @Composable @@ -90,9 +89,9 @@ class CallScreenPresenter( DisposableEffect(Unit) { coroutineScope.launch { // Sets the call as joined - activeCallManager.joinedCall(callType) + activeCallManager.joinedCall(callData) fetchRoomCallUrl( - inputs = callType, + callData = callData, urlState = urlState, callWidgetDriver = callWidgetDriver, languageTag = languageTag, @@ -100,19 +99,10 @@ class CallScreenPresenter( ) } onDispose { - appCoroutineScope.launch { activeCallManager.hangUpCall(callType) } + appCoroutineScope.launch { activeCallManager.hangUpCall(callData) } } } - - when (callType) { - is CallType.ExternalUrl -> { - // No analytics yet for external calls - } - is CallType.RoomCall -> { - screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) - } - } - + screenTracker.TrackScreen(screen = MobileScreen.ScreenName.RoomCall) HandleMatrixClientSyncState() callWidgetDriver.value?.let { driver -> @@ -149,25 +139,22 @@ class CallScreenPresenter( .launchIn(this) } - if (callType is CallType.RoomCall) { - // Note: For external calls isWidgetLoaded will always be false - LaunchedEffect(Unit) { - // Wait for the call to be joined, if it takes too long, we display an error - delay(10.seconds) + LaunchedEffect(Unit) { + // Wait for the call to be joined, if it takes too long, we display an error + delay(10.seconds) - if (!isWidgetLoaded) { - Timber.w("The call took too long to load. Displaying an error before exiting.") + if (!isWidgetLoaded) { + Timber.w("The call took too long to load. Displaying an error before exiting.") - // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call - webViewError = "" - } + // This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call + webViewError = "" } } } - fun handleEvent(event: CallScreenEvents) { + fun handleEvent(event: CallScreenEvent) { when (event) { - is CallScreenEvents.Hangup -> { + is CallScreenEvent.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value if (widgetId != null && interceptor != null && isWidgetLoaded) { @@ -187,10 +174,10 @@ class CallScreenPresenter( } } } - is CallScreenEvents.SetupMessageChannels -> { + is CallScreenEvent.SetupMessageChannels -> { messageInterceptor.value = event.widgetMessageInterceptor } - is CallScreenEvents.OnWebViewError -> { + is CallScreenEvent.OnWebViewError -> { if (!ignoreWebViewError) { webViewError = event.description.orEmpty() } @@ -204,37 +191,29 @@ class CallScreenPresenter( webViewError = webViewError, userAgent = userAgent, isCallActive = isWidgetLoaded, - isInWidgetMode = isInWidgetMode, eventSink = ::handleEvent, ) } private suspend fun fetchRoomCallUrl( - inputs: CallType, + callData: CallData, urlState: MutableState>, callWidgetDriver: MutableState, languageTag: String?, theme: String?, ) { urlState.runCatchingUpdatingState { - when (inputs) { - is CallType.ExternalUrl -> { - inputs.url - } - is CallType.RoomCall -> { - val result = callWidgetProvider.getWidget( - sessionId = inputs.sessionId, - roomId = inputs.roomId, - clientId = UUID.randomUUID().toString(), - isAudioCall = inputs.isAudioCall, - languageTag = languageTag, - theme = theme, - ).getOrThrow() - callWidgetDriver.value = result.driver - Timber.d("Call widget driver initialized for sessionId: ${inputs.sessionId}, roomId: ${inputs.roomId}") - result.url - } - } + val result = callWidgetProvider.getWidget( + sessionId = callData.sessionId, + roomId = callData.roomId, + clientId = UUID.randomUUID().toString(), + isAudioCall = callData.isAudioCall, + languageTag = languageTag, + theme = theme, + ).getOrThrow() + callWidgetDriver.value = result.driver + Timber.d("Call widget driver initialized for sessionId: ${callData.sessionId}, roomId: ${callData.roomId}") + result.url } } @@ -242,12 +221,11 @@ class CallScreenPresenter( private fun HandleMatrixClientSyncState() { val coroutineScope = rememberCoroutineScope() DisposableEffect(Unit) { - val roomCallType = callType as? CallType.RoomCall ?: return@DisposableEffect onDispose {} - val client = matrixClientsProvider.getOrNull(roomCallType.sessionId) ?: return@DisposableEffect onDispose { - Timber.w("No MatrixClient found for sessionId, can't send call notification: ${roomCallType.sessionId}") + val client = matrixClientsProvider.getOrNull(callData.sessionId) ?: return@DisposableEffect onDispose { + Timber.w("No MatrixClient found for sessionId, can't send call notification: ${callData.sessionId}") } coroutineScope.launch { - Timber.d("Observing sync state in-call for sessionId: ${roomCallType.sessionId}") + Timber.d("Observing sync state in-call for sessionId: ${callData.sessionId}") client.syncService.syncState .collect { state -> if (state != SyncState.Running) { @@ -256,7 +234,7 @@ class CallScreenPresenter( } } onDispose { - Timber.d("Stopped observing sync state in-call for sessionId: ${roomCallType.sessionId}") + Timber.d("Stopped observing sync state in-call for sessionId: ${callData.sessionId}") // Make sure we mark the call as ended in the app state appForegroundStateService.updateIsInCallState(false) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt index c07594aebb..86b4cc439f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenState.kt @@ -15,6 +15,5 @@ data class CallScreenState( val webViewError: String?, val userAgent: String, val isCallActive: Boolean, - val isInWidgetMode: Boolean, - val eventSink: (CallScreenEvents) -> Unit, + val eventSink: (CallScreenEvent) -> Unit, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt index 3e72f96f87..155c5d3380 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt @@ -26,15 +26,13 @@ internal fun aCallScreenState( webViewError: String? = null, userAgent: String = "", isCallActive: Boolean = true, - isInWidgetMode: Boolean = false, - eventSink: (CallScreenEvents) -> Unit = {}, + eventSink: (CallScreenEvent) -> Unit = {}, ): CallScreenState { return CallScreenState( urlState = urlState, webViewError = webViewError, userAgent = userAgent, isCallActive = isCallActive, - isInWidgetMode = isInWidgetMode, eventSink = eventSink, ) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index f8657a9ece..2537eb739c 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -17,9 +17,10 @@ import android.webkit.WebChromeClient import android.webkit.WebView import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,7 +34,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView import io.element.android.features.call.impl.R -import io.element.android.features.call.impl.pip.PictureInPictureEvents +import io.element.android.features.call.impl.pip.PictureInPictureEvent import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.utils.InvalidAudioDeviceReason @@ -45,7 +46,6 @@ import io.element.android.libraries.designsystem.components.ProgressDialog 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.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import timber.log.Timber @@ -64,94 +64,93 @@ internal fun CallScreenView( requestPermissions: (Array, RequestPermissionCallback) -> Unit, modifier: Modifier = Modifier, ) { - fun handleBack() { - if (pipState.supportPip) { - pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture) - } else { - state.eventSink(CallScreenEvents.Hangup) + var callWebView by remember { mutableStateOf(null) } + + fun handleBack(fromNative: Boolean = false) { + when (CallScreenBackPressPolicy.resolve(supportPip = pipState.supportPip, hasWebView = callWebView != null, fromNative)) { + CallScreenBackPressAction.EnterPictureInPicture -> + pipState.eventSink(PictureInPictureEvent.EnterPictureInPicture) + CallScreenBackPressAction.DispatchEscapeToWebView -> + callWebView?.dispatchEscKeyEvent() + null -> Timber.d("Back press with unsupported pip is a no-op") } } - Scaffold( - modifier = modifier, - ) { padding -> - BackHandler { - handleBack() + BackHandler { + handleBack(fromNative = true) + } + if (state.webViewError != null) { + ErrorDialog( + content = buildString { + append(stringResource(CommonStrings.error_unknown)) + state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) } + }, + onSubmit = { state.eventSink(CallScreenEvent.Hangup) }, + ) + } else { + var webViewAudioManager by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + + var invalidAudioDeviceReason by remember { mutableStateOf(null) } + invalidAudioDeviceReason?.let { + InvalidAudioDeviceDialog(invalidAudioDeviceReason = it) { + invalidAudioDeviceReason = null + } } - if (state.webViewError != null) { - ErrorDialog( - content = buildString { - append(stringResource(CommonStrings.error_unknown)) - state.webViewError.takeIf { it.isNotEmpty() }?.let { append("\n\n").append(it) } - }, - onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, - ) - } else { - var webViewAudioManager by remember { mutableStateOf(null) } - val coroutineScope = rememberCoroutineScope() - var invalidAudioDeviceReason by remember { mutableStateOf(null) } - invalidAudioDeviceReason?.let { - InvalidAudioDeviceDialog(invalidAudioDeviceReason = it) { - invalidAudioDeviceReason = null - } + CallWebView( + modifier = modifier.consumeWindowInsets(WindowInsets.systemBars).fillMaxSize(), + url = state.urlState, + userAgent = state.userAgent, + onPermissionsRequest = { request -> + val androidPermissions = mapWebkitPermissions(request.resources) + val callback: RequestPermissionCallback = { request.grant(it) } + requestPermissions(androidPermissions.toTypedArray(), callback) + }, + onConsoleMessage = onConsoleMessage, + onCreateWebView = { webView -> + callWebView = webView + webView.addBackHandler(onBackPressed = ::handleBack) + val interceptor = WebViewWidgetMessageInterceptor( + webView = webView, + onUrlLoaded = { url -> + webView.evaluateJavascript("controls.onBackButtonPressed = () => { backHandler.onBackPressed() }", null) + if (webViewAudioManager?.isInCallMode?.get() == false) { + Timber.d("URL $url is loaded, starting in-call audio mode") + webViewAudioManager?.onCallStarted() + } else { + Timber.d("Can't start in-call audio mode since the app is already in it.") + } + }, + onError = { state.eventSink(CallScreenEvent.OnWebViewError(it)) }, + ) + webViewAudioManager = WebViewAudioManager( + webView = webView, + coroutineScope = coroutineScope, + onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it }, + ) + state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor)) + val pipController = WebViewPipController(webView) + pipState.eventSink(PictureInPictureEvent.SetPipController(pipController)) + }, + onDestroyWebView = { + callWebView = null + // Reset audio mode + webViewAudioManager?.onCallStopped() } - - CallWebView( - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .fillMaxSize(), - url = state.urlState, - userAgent = state.userAgent, - onPermissionsRequest = { request -> - val androidPermissions = mapWebkitPermissions(request.resources) - val callback: RequestPermissionCallback = { request.grant(it) } - requestPermissions(androidPermissions.toTypedArray(), callback) - }, - onConsoleMessage = onConsoleMessage, - onCreateWebView = { webView -> - webView.addBackHandler(onBackPressed = ::handleBack) - val interceptor = WebViewWidgetMessageInterceptor( - webView = webView, - onUrlLoaded = { url -> - webView.evaluateJavascript("controls.onBackButtonPressed = () => { backHandler.onBackPressed() }", null) - if (webViewAudioManager?.isInCallMode?.get() == false) { - Timber.d("URL $url is loaded, starting in-call audio mode") - webViewAudioManager?.onCallStarted() - } else { - Timber.d("Can't start in-call audio mode since the app is already in it.") - } - }, - onError = { state.eventSink(CallScreenEvents.OnWebViewError(it)) }, - ) - webViewAudioManager = WebViewAudioManager( - webView = webView, - coroutineScope = coroutineScope, - onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it }, - ) - state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) - val pipController = WebViewPipController(webView) - pipState.eventSink(PictureInPictureEvents.SetPipController(pipController)) - }, - onDestroyWebView = { - // Reset audio mode - webViewAudioManager?.onCallStopped() - } - ) - when (state.urlState) { - AsyncData.Uninitialized, - is AsyncData.Loading -> - ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) - is AsyncData.Failure -> { - Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}") - ErrorDialog( - content = state.urlState.error.message.orEmpty(), - onSubmit = { state.eventSink(CallScreenEvents.Hangup) }, - ) - } - is AsyncData.Success -> Unit + ) + when (state.urlState) { + AsyncData.Uninitialized, + is AsyncData.Loading -> + ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) + is AsyncData.Failure -> { + Timber.e(state.urlState.error, "WebView failed to load URL: ${state.urlState.error.message}") + ErrorDialog( + content = state.urlState.error.message.orEmpty(), + onSubmit = { state.eventSink(CallScreenEvent.Hangup) }, + ) } + is AsyncData.Success -> Unit } } } @@ -248,15 +247,16 @@ private fun WebView.setup( private fun WebView.addBackHandler(onBackPressed: () -> Unit) { addJavascriptInterface( - object { - @Suppress("unused") - @JavascriptInterface - fun onBackPressed() = onBackPressed() - }, + JavascriptBackHandlerBridge(callback = onBackPressed), "backHandler" ) } +private fun WebView.dispatchEscKeyEvent() { + dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_ESCAPE)) + dispatchKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_ESCAPE)) +} + @PreviewsDayNight @Composable internal fun CallScreenViewPreview( @@ -275,3 +275,12 @@ internal fun CallScreenViewPreview( internal fun InvalidAudioDeviceDialogPreview() = ElementPreview { InvalidAudioDeviceDialog(invalidAudioDeviceReason = InvalidAudioDeviceReason.BT_AUDIO_DEVICE_DISABLED) {} } + +internal class JavascriptBackHandlerBridge( + private val callback: () -> Unit, +) { + @JavascriptInterface + fun onBackPressed() { + callback() + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt deleted file mode 100644 index 0c18c3e1a4..0000000000 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallTypeExtension.kt +++ /dev/null @@ -1,19 +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.call.impl.ui - -import io.element.android.features.call.api.CallType -import io.element.android.libraries.matrix.api.core.SessionId - -fun CallType.getSessionId(): SessionId? { - return when (this) { - is CallType.ExternalUrl -> null - is CallType.RoomCall -> sessionId - } -} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index 5fa3beb36a..26df7c160c 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -32,19 +32,20 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.core.app.PictureInPictureModeChangedInfo import androidx.core.content.IntentCompat import androidx.core.util.Consumer +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import dev.zacsweers.metro.Inject import io.element.android.compound.colors.SemanticColorsLightDark -import io.element.android.features.call.api.CallType -import io.element.android.features.call.api.CallType.ExternalUrl +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings -import io.element.android.features.call.impl.pip.PictureInPictureEvents +import io.element.android.features.call.impl.pip.PictureInPictureEvent import io.element.android.features.call.impl.pip.PictureInPicturePresenter import io.element.android.features.call.impl.pip.PictureInPictureState import io.element.android.features.call.impl.pip.PipView import io.element.android.features.call.impl.services.CallForegroundService -import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger import io.element.android.libraries.architecture.Presenter @@ -54,6 +55,7 @@ import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.theme.ElementThemeApp +import io.element.android.libraries.designsystem.utils.hasCompactHeightWindowSize import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.preferences.api.store.AppPreferencesStore import timber.log.Timber @@ -64,7 +66,6 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator, PipView { - @Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var presenterFactory: CallScreenPresenter.Factory @Inject lateinit var appPreferencesStore: AppPreferencesStore @Inject lateinit var featureFlagService: FeatureFlagService @@ -80,9 +81,9 @@ class ElementCallActivity : private val requestPermissionsLauncher = registerPermissionResultLauncher() - private val webViewTarget = mutableStateOf(null) + private val webViewTarget = mutableStateOf(null) - private var eventSink: ((CallScreenEvents) -> Unit)? = null + private var eventSink: ((CallScreenEvent) -> Unit)? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -98,7 +99,7 @@ class ElementCallActivity : window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) } - setCallType(intent) + setCallData(intent) // If presenter is not created at this point, it means we have no call to display, the Activity is finishing, so return early if (!::presenter.isInitialized) { return @@ -111,9 +112,30 @@ class ElementCallActivity : setContent { val pipState = pictureInPicturePresenter.present() ListenToAndroidEvents(pipState) - val colors by remember(webViewTarget.value?.getSessionId()) { - enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.getSessionId()) + val colors by remember(webViewTarget.value?.sessionId) { + enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.sessionId) }.collectAsState(SemanticColorsLightDark.default) + + // When the height is compact, hide the system bars by default to maximize the space for the call, using immersive mode + val hasCompactHeight = hasCompactHeightWindowSize() + DisposableEffect(hasCompactHeight, pipState.isInPictureInPicture) { + if (hasCompactHeight && !pipState.isInPictureInPicture) { + val window = this@ElementCallActivity.window ?: return@DisposableEffect onDispose {} + val insetsController = WindowCompat.getInsetsController(window, window.decorView) + val systemBarInsets = WindowInsetsCompat.Type.systemBars() + insetsController.hide(systemBarInsets) + + insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + onDispose { + insetsController.show(systemBarInsets) + insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } + } else { + onDispose {} + } + } + ElementThemeApp( appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService, @@ -123,9 +145,8 @@ class ElementCallActivity : ) { val state = presenter.present() eventSink = state.eventSink - LaunchedEffect(state.isCallActive, state.isInWidgetMode) { - // Note when not in WidgetMode, isCallActive will never be true, so consider the call is active - if (state.isCallActive || !state.isInWidgetMode) { + LaunchedEffect(state.isCallActive) { + if (state.isCallActive) { setCallIsActive() } } @@ -163,7 +184,7 @@ class ElementCallActivity : if (requestPermissionCallback != null) { Timber.tag(loggerTag.value).w("Ignoring onUserLeaveHint event because user is asked to grant permissions") } else { - pipEventSink(PictureInPictureEvents.EnterPictureInPicture) + pipEventSink(PictureInPictureEvent.EnterPictureInPicture) } } addOnUserLeaveHintListener(listener) @@ -173,10 +194,10 @@ class ElementCallActivity : } DisposableEffect(Unit) { val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo -> - pipEventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode)) + pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode)) if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call") - eventSink?.invoke(CallScreenEvents.Hangup) + eventSink?.invoke(CallScreenEvent.Hangup) } } addOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener) @@ -188,7 +209,7 @@ class ElementCallActivity : override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - setCallType(intent) + setCallData(intent) } override fun onDestroy() { @@ -207,25 +228,24 @@ class ElementCallActivity : finish() } - private fun setCallType(intent: Intent?) { - val callType = intent?.let { - IntentCompat.getParcelableExtra(intent, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallType::class.java) - ?: intent.dataString?.let(::parseUrl)?.let(::ExternalUrl) + private fun setCallData(intent: Intent?) { + val callData = intent?.let { + IntentCompat.getParcelableExtra(intent, DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, CallData::class.java) } - val currentCallType = webViewTarget.value - if (currentCallType == null) { - if (callType == null) { + val currentCallData = webViewTarget.value + if (currentCallData == null) { + if (callData == null) { Timber.tag(loggerTag.value).d("Re-opened the activity but we have no url to load or a cached one, finish the activity") finish() } else { Timber.tag(loggerTag.value).d("Set the call type and create the presenter") - webViewTarget.value = callType - presenter = presenterFactory.create(callType, this) + webViewTarget.value = callData + presenter = presenterFactory.create(callData, this) } } else { - if (callType == null) { + if (callData == null) { Timber.tag(loggerTag.value).d("Coming back from notification, do nothing") - } else if (callType != currentCallType) { + } else if (callData != currentCallData) { Timber.tag(loggerTag.value).d("User starts another call, restart the Activity") setIntent(intent) recreate() @@ -236,8 +256,6 @@ class ElementCallActivity : } } - private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url) - private fun registerPermissionResultLauncher(): ActivityResultLauncher> { return registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() @@ -287,7 +305,7 @@ class ElementCallActivity : } override fun hangUp() { - eventSink?.invoke(CallScreenEvents.Hangup) + eventSink?.invoke(CallScreenEvent.Hangup) } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt index 2c4deab65e..1d6989fb3c 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt @@ -19,7 +19,7 @@ import androidx.core.content.IntentCompat import androidx.lifecycle.lifecycleScope import dev.zacsweers.metro.Inject import io.element.android.compound.colors.SemanticColorsLightDark -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.notifications.CallNotificationData @@ -118,10 +118,10 @@ class IncomingCallActivity : AppCompatActivity() { private fun onAnswer(notificationData: CallNotificationData) { elementCallEntryPoint.startCall( - CallType.RoomCall( - notificationData.sessionId, - notificationData.roomId, - isAudioCall = notificationData.audioOnly + CallData( + sessionId = notificationData.sessionId, + roomId = notificationData.roomId, + isAudioCall = notificationData.audioOnly, ) ) } @@ -129,7 +129,7 @@ class IncomingCallActivity : AppCompatActivity() { private fun onCancel() { val activeCall = activeCallManager.activeCall.value ?: return appCoroutineScope.launch { - activeCallManager.hangUpCall(callType = activeCall.callType) + activeCallManager.hangUpCall(callData = activeCall.callData) } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index 99679a8afb..685fc932fe 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -20,7 +20,7 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.ElementCallConfig -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.CurrentCall import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator @@ -73,20 +73,20 @@ interface ActiveCallManager { /** * Called to hang up the active call. It will hang up the call and remove any existing UI and the active call. - * @param callType The type of call that the user hangs up, either an external url one or a room one. + * @param callData The data about the call. * @param notificationData The data for the incoming call notification. */ suspend fun hangUpCall( - callType: CallType, + callData: CallData, notificationData: CallNotificationData? = null, ) /** * Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall]. * - * @param callType The type of call that the user joined, either an external url one or a room one. + * @param callData The data about the call. */ - suspend fun joinedCall(callType: CallType) + suspend fun joinedCall(callData: CallData) } @SingleIn(AppScope::class) @@ -143,7 +143,7 @@ class DefaultActiveCallManager( return } activeCall.value = ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = notificationData.sessionId, roomId = notificationData.roomId, isAudioCall = notificationData.audioOnly, @@ -198,17 +198,17 @@ class DefaultActiveCallManager( } override suspend fun hangUpCall( - callType: CallType, + callData: CallData, notificationData: CallNotificationData?, ) = mutex.withLock { - Timber.tag(tag).d("Hang up call: $callType") + Timber.tag(tag).d("Hang up call: $callData") cancelIncomingCallNotification() val currentActiveCall = activeCall.value ?: run { // activeCall.value can be null if the application has been killed while the call was ringing // Build a currentActiveCall with the provided parameters. notificationData?.let { ActiveCall( - callType = callType, + callData = callData, callState = CallState.Ringing( notificationData = notificationData, ) @@ -219,8 +219,8 @@ class DefaultActiveCallManager( return@withLock } - if (currentActiveCall.callType != callType) { - Timber.tag(tag).w("Call type $callType does not match the active call type, ignoring") + if (currentActiveCall.callData != callData) { + Timber.tag(tag).w("Call type $callData does not match the active call type, ignoring") return@withLock } if (currentActiveCall.callState is CallState.Ringing) { @@ -244,8 +244,8 @@ class DefaultActiveCallManager( activeCall.value = null } - override suspend fun joinedCall(callType: CallType) = mutex.withLock { - Timber.tag(tag).d("Joined call: $callType") + override suspend fun joinedCall(callData: CallData) = mutex.withLock { + Timber.tag(tag).d("Joined call: $callData") cancelIncomingCallNotification() if (activeWakeLock?.isHeld == true) { Timber.tag(tag).d("Releasing partial wakelock after joining call") @@ -254,7 +254,7 @@ class DefaultActiveCallManager( timedOutCallJob?.cancel() activeCall.value = ActiveCall( - callType = callType, + callData = callData, callState = CallState.InCall, ) } @@ -307,15 +307,15 @@ class DefaultActiveCallManager( private fun observeRingingCall() { activeCall .filterNotNull() - .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .filter { it.callState is CallState.Ringing } .flatMapLatest { activeCall -> - val callType = activeCall.callType as CallType.RoomCall + val callData = activeCall.callData val ringingInfo = activeCall.callState as CallState.Ringing - val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { + val client = matrixClientProvider.getOrRestore(callData.sessionId).getOrNull() ?: run { Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") return@flatMapLatest flowOf() } - val room = client.getRoom(callType.roomId) ?: run { + val room = client.getRoom(callData.roomId) ?: run { Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") return@flatMapLatest flowOf() } @@ -346,17 +346,17 @@ class DefaultActiveCallManager( // has joined the call from another session. activeCall .filterNotNull() - .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } + .filter { it.callState is CallState.Ringing } .flatMapLatest { activeCall -> - val callType = activeCall.callType as CallType.RoomCall + val callData = activeCall.callData // Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room - val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.roomId) ?: run { + val room = matrixClientProvider.getOrRestore(callData.sessionId).getOrNull()?.getRoom(callData.roomId) ?: run { Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") return@flatMapLatest flowOf() } room.roomInfoFlow.map { Timber.tag(tag).d("Has room call status changed for ringing call: ${it.hasRoomCall}") - it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants) + it.hasRoomCall to (callData.sessionId in it.activeRoomCallParticipants) } } // We only want to check if the room active call status changes @@ -388,10 +388,7 @@ class DefaultActiveCallManager( // Nothing to do } is CallState.InCall -> { - when (val callType = value.callType) { - is CallType.ExternalUrl -> defaultCurrentCallService.onCallStarted(CurrentCall.ExternalUrl(callType.url)) - is CallType.RoomCall -> defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(callType.roomId)) - } + defaultCurrentCallService.onCallStarted(CurrentCall.RoomCall(value.callData.roomId)) } } } @@ -404,7 +401,7 @@ class DefaultActiveCallManager( * Represents an active call. */ data class ActiveCall( - val callType: CallType, + val callData: CallData, val callState: CallState, ) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt deleted file mode 100644 index f5433c15a0..0000000000 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-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.call.impl.utils - -import android.net.Uri -import androidx.core.net.toUri -import dev.zacsweers.metro.Inject - -@Inject -class CallIntentDataParser { - private val validHttpSchemes = sequenceOf("https") - private val knownHosts = sequenceOf( - "call.element.io", - ) - - fun parse(data: String?): String? { - val parsedUrl = data?.toUri() ?: return null - val scheme = parsedUrl.scheme - return when { - scheme in validHttpSchemes -> parsedUrl - scheme == "element" && parsedUrl.host == "call" -> { - parsedUrl.getUrlParameter() - } - scheme == "io.element.call" && parsedUrl.host == null -> { - parsedUrl.getUrlParameter() - } - // This should never be possible, but we still need to take into account the possibility - else -> null - } - ?.takeIf { it.host in knownHosts } - ?.withCustomParameters() - } - - private fun Uri.getUrlParameter(): Uri? { - return getQueryParameter("url") - ?.let { urlParameter -> - urlParameter.toUri().takeIf { uri -> - uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank() - } - } - } -} - -/** - * Ensure the uri has the following parameters and value in the fragment: - * - appPrompt=false - * - confineToRoom=true - * to ensure that the rendering will bo correct on the embedded Webview. - */ -private fun Uri.withCustomParameters(): String { - val builder = buildUpon() - // Remove the existing query parameters - builder.clearQuery() - queryParameterNames.forEach { - if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach - builder.appendQueryParameter(it, getQueryParameter(it)) - } - // Remove the existing fragment parameters, and build the new fragment - val currentFragment = fragment ?: "" - // Reset the current fragment - builder.fragment("") - val queryFragmentPosition = currentFragment.lastIndexOf("?") - val newFragment = if (queryFragmentPosition == -1) { - // No existing query, build it. - "$currentFragment?$APP_PROMPT_PARAMETER=false&$CONFINE_TO_ROOM_PARAMETER=true" - } else { - buildString { - append(currentFragment.substring(0, queryFragmentPosition + 1)) - val queryFragment = currentFragment.substring(queryFragmentPosition + 1) - // Replace the existing parameters - val newQueryFragment = queryFragment - .replace("$APP_PROMPT_PARAMETER=true", "$APP_PROMPT_PARAMETER=false") - .replace("$CONFINE_TO_ROOM_PARAMETER=false", "$CONFINE_TO_ROOM_PARAMETER=true") - append(newQueryFragment) - // Ensure the parameters are there - if (!newQueryFragment.contains("$APP_PROMPT_PARAMETER=false")) { - if (newQueryFragment.isNotEmpty()) { - append("&") - } - append("$APP_PROMPT_PARAMETER=false") - } - if (!newQueryFragment.contains("$CONFINE_TO_ROOM_PARAMETER=true")) { - append("&$CONFINE_TO_ROOM_PARAMETER=true") - } - } - } - // We do not want to encode the Fragment part, so append it manually - return builder.build().toString() + "#" + newFragment -} - -private const val APP_PROMPT_PARAMETER = "appPrompt" -private const val CONFINE_TO_ROOM_PARAMETER = "confineToRoom" diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt index b31b6152d0..8de7b81d6d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt @@ -14,7 +14,6 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.MatrixClientProvider 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.isDm import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.services.appnavstate.api.ActiveRoomsHolder diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt index 0f74ba86d4..c6c607cbbc 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/IntentProvider.kt @@ -12,21 +12,21 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.PendingIntentCompat -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.ui.ElementCallActivity internal object IntentProvider { - fun createIntent(context: Context, callType: CallType): Intent = Intent(context, ElementCallActivity::class.java).apply { - putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callType) + fun createIntent(context: Context, callData: CallData): Intent = Intent(context, ElementCallActivity::class.java).apply { + putExtra(DefaultElementCallEntryPoint.EXTRA_CALL_TYPE, callData) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION) } - fun getPendingIntent(context: Context, callType: CallType): PendingIntent { + fun getPendingIntent(context: Context, callData: CallData): PendingIntent { return PendingIntentCompat.getActivity( context, DefaultElementCallEntryPoint.REQUEST_CODE, - createIntent(context, callType), + createIntent(context, callData), PendingIntent.FLAG_CANCEL_CURRENT, false )!! diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt index 2d674d21af..e68e57118b 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt @@ -201,16 +201,12 @@ class WebViewAudioManager( return } - coroutineScope.launch { - proximitySensorMutex.withLock { - if (proximitySensorWakeLock?.isHeld == true) { - proximitySensorWakeLock?.release() - } - } + // Since this should run when the call is no longer running, it should be OK to not use the mutex here + if (proximitySensorWakeLock?.isHeld == true) { + proximitySensorWakeLock?.release() } audioManager.mode = AudioManager.MODE_NORMAL - if (!hasRegisteredCallbacks) { Timber.w("Audio: tried to disable webview in-call audio mode without registering callbacks") return @@ -268,8 +264,8 @@ class WebViewAudioManager( private fun setWebViewAndroidNativeBridge() { Timber.d("Adding callback in controls.onAudioPlaybackStarted") webView.evaluateJavascript("controls.onAudioPlaybackStarted = () => { androidNativeBridge.onTrackReady(); };", null) - Timber.d("Adding callback in controls.onOutputDeviceSelect") - webView.evaluateJavascript("controls.onOutputDeviceSelect = (id) => { androidNativeBridge.setOutputDevice(id); };", null) + Timber.d("Adding callback in controls.onAudioDeviceSelect") + webView.evaluateJavascript("controls.onAudioDeviceSelect = (id) => { androidNativeBridge.setAudioDevice(id); };", null) } /** @@ -296,8 +292,8 @@ class WebViewAudioManager( ) { Timber.d("Updating available audio devices") val deviceList = json.encodeToString(devices) - webView.evaluateJavascript("controls.setAvailableOutputDevices($deviceList);", { - Timber.d("Audio: setAvailableOutputDevices result: $it") + webView.evaluateJavascript("controls.setAvailableAudioDevices($deviceList);", { + Timber.d("Audio: setAvailableAudioDevices result: $it") }) } @@ -345,6 +341,7 @@ class WebViewAudioManager( } else { // On Android 11 and lower, we don't have the concept of communication devices // We have to call the right methods based on the device type + @Suppress("DEPRECATION") if (device != null) { if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO && disableBluetoothAudioDevices) { Timber.w("Bluetooth audio devices are disabled on this Android version") @@ -353,11 +350,9 @@ class WebViewAudioManager( return } setAudioEnabled(true) - @Suppress("DEPRECATION") isSpeakerphoneOn = device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER isBluetoothScoOn = device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } else { - @Suppress("DEPRECATION") isSpeakerphoneOn = false isBluetoothScoOn = false } @@ -402,7 +397,7 @@ private class AndroidWebViewAudioBridge( private val onAudioPlaybackStarted: () -> Unit, ) { @JavascriptInterface - fun setOutputDevice(id: String) { + fun setAudioDevice(id: String) { Timber.d("Audio device selected in webview, id: $id") onAudioDeviceSelected(id) } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt index f7ab2c57af..c74ae90abd 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt @@ -140,26 +140,33 @@ class WebViewWidgetMessageInterceptor( } } - // Create a WebMessageListener, which will receive messages from the WebView and reply to them - val webMessageListener = WebViewCompat.WebMessageListener { _, message, _, _, _ -> - onMessageReceived(message.data) - } + // Always register JavascriptInterface as the baseline message channel. + // This works on all WebView implementations including Huawei. + webView.addJavascriptInterface(object { + @JavascriptInterface + fun postMessage(json: String?) { + onMessageReceived(json) + } + }, LISTENER_NAME) - // Use WebMessageListener if supported, otherwise use JavascriptInterface - if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + // Additionally register WebMessageListener on WebViews that reliably support it. + // Huawei WebView (Chromium < 119) reports WEB_MESSAGE_LISTENER as supported + // but silently drops messages, so we only trust it on Chromium 119+. + // See: https://github.com/element-hq/element-x-android/issues/6632 + val webViewVersionName = WebViewCompat.getCurrentWebViewPackage(webView.context)?.versionName.orEmpty() + Timber.d("Using WebView version: $webViewVersionName") + val webViewVersionCode = webViewVersionName.split(".").firstOrNull()?.toIntOrNull() ?: 0 + + if (webViewVersionCode >= 119 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener( webView, LISTENER_NAME, setOf("*"), - webMessageListener - ) - } else { - webView.addJavascriptInterface(object { - @JavascriptInterface - fun postMessage(json: String?) { - onMessageReceived(json) + WebViewCompat.WebMessageListener { _, message, _, _, _ -> + onMessageReceived(message.data) } - }, LISTENER_NAME) + ) } } diff --git a/features/call/impl/src/main/res/values-ca/translations.xml b/features/call/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..92c518dbee --- /dev/null +++ b/features/call/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "Trucada en curs" + "Toca per tornar a la trucada" + "☎️ Trucada en curs" + "Element Call entrant" + diff --git a/features/call/impl/src/main/res/values-et/translations.xml b/features/call/impl/src/main/res/values-et/translations.xml index 16b72b8b97..fa0415a5f5 100644 --- a/features/call/impl/src/main/res/values-et/translations.xml +++ b/features/call/impl/src/main/res/values-et/translations.xml @@ -4,5 +4,5 @@ "Kõne juurde naasmiseks klõpsa" "☎️ Kõne on pooleli" "Element Call ei võimalda selles Androidi versioonis Bluetoothi heliseadmete kasutamist. Palun vali mõni muu heliseade." - "Sissetulev Element Calli kõne" + "Saabuv Element Calli kõne" diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt index 85cec8c586..9b2ee3d923 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt @@ -11,7 +11,7 @@ package io.element.android.features.call import android.content.Intent 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.api.CallData import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.ui.ElementCallActivity @@ -21,23 +21,21 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows.shadowOf import kotlin.time.Duration.Companion.seconds -@RunWith(RobolectricTestRunner::class) -class DefaultElementCallEntryPointTest { +class DefaultElementCallEntryPointTest : RobolectricTest() { @Test fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest { val entryPoint = createEntryPoint() - entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false)) + entryPoint.startCall(CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false)) val expectedIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, ElementCallActivity::class.java) val intent = shadowOf(RuntimeEnvironment.getApplication()).nextStartedActivity @@ -53,7 +51,7 @@ class DefaultElementCallEntryPointTest { val entryPoint = createEntryPoint(activeCallManager = activeCallManager) entryPoint.handleIncomingCall( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, isAudioCall = false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, isAudioCall = false), eventId = AN_EVENT_ID, senderId = A_USER_ID_2, roomName = "roomName", diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt index c087fa3c35..c3d7fdf17b 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt @@ -8,11 +8,9 @@ package io.element.android.features.call.impl.pip -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Test @@ -20,9 +18,7 @@ class PictureInPicturePresenterTest { @Test fun `when pip is not supported, the state value supportPip is false`() = runTest { val presenter = createPictureInPicturePresenter(supportPip = false) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.supportPip).isFalse() } @@ -35,9 +31,7 @@ class PictureInPicturePresenterTest { supportPip = true, pipView = FakePipView(setPipParamsResult = { }), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.supportPip).isTrue() } @@ -53,18 +47,16 @@ class PictureInPicturePresenterTest { enterPipModeResult = enterPipModeResult, ), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.isInPictureInPicture).isFalse() - initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) enterPipModeResult.assertions().isCalledOnce() - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(true)) val pipState = awaitItem() assertThat(pipState.isInPictureInPicture).isTrue() // User stops pip - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(false)) val finalState = awaitItem() assertThat(finalState.isInPictureInPicture).isFalse() } @@ -80,12 +72,10 @@ class PictureInPicturePresenterTest { handUpResult = handUpResult ), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() - initialState.eventSink(PictureInPictureEvents.SetPipController(FakePipController(canEnterPipResult = { false }))) - initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvent.SetPipController(FakePipController(canEnterPipResult = { false }))) + initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) handUpResult.assertions().isCalledOnce() } } @@ -102,12 +92,10 @@ class PictureInPicturePresenterTest { enterPipModeResult = enterPipModeResult ), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink( - PictureInPictureEvents.SetPipController( + PictureInPictureEvent.SetPipController( FakePipController( canEnterPipResult = { true }, enterPipResult = enterPipResult, @@ -115,16 +103,16 @@ class PictureInPicturePresenterTest { ) ) ) - initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture) + initialState.eventSink(PictureInPictureEvent.EnterPictureInPicture) enterPipModeResult.assertions().isCalledOnce() enterPipResult.assertions().isNeverCalled() - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(true)) val pipState = awaitItem() assertThat(pipState.isInPictureInPicture).isTrue() enterPipResult.assertions().isCalledOnce() // User stops pip exitPipResult.assertions().isNeverCalled() - initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false)) + initialState.eventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(false)) val finalState = awaitItem() assertThat(finalState.isInPictureInPicture).isFalse() exitPipResult.assertions().isCalledOnce() diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt index c055b408dd..3f5c1c4e50 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt @@ -23,13 +23,11 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class RingingCallNotificationCreatorTest { +class RingingCallNotificationCreatorTest : RobolectricTest() { @Test fun `createNotification - with no associated MatrixClient does nothing`() = runTest { val notificationCreator = createRingingCallNotificationCreator( diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt new file mode 100644 index 0000000000..f0cdd44082 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallDataTest.kt @@ -0,0 +1,23 @@ +/* + * 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.call.ui + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.api.CallData +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import org.junit.Test + +class CallDataTest { + @Test + fun `RoomCall stringification does not contain the URL`() { + assertThat(CallData(A_SESSION_ID, A_ROOM_ID, false).toString()) + .isEqualTo("CallData(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)") + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt new file mode 100644 index 0000000000..f07f7039d3 --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenBackPressPolicyTest.kt @@ -0,0 +1,96 @@ +/* + * 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.call.ui + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.impl.ui.CallScreenBackPressAction +import io.element.android.features.call.impl.ui.CallScreenBackPressPolicy +import org.junit.Test + +class CallScreenBackPressPolicyTest { + @Test + fun `resolve returns dispatch escape when a web view is available and native button is pressed`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = true, + fromNative = true, + ) + + assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView) + } + + @Test + fun `resolve dispatch escape when there is a web view and pip is supported on native button press`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = true, + fromNative = true, + ) + + assertThat(result).isEqualTo(CallScreenBackPressAction.DispatchEscapeToWebView) + } + + @Test + fun `resolve returns hangup when there is no web view and pip is not supported from native button`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = false, + fromNative = true, + ) + + assertThat(result).isNull() + } + + @Test + fun `resolve returns hangup when there is no web view even though pip is supported from native button`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = false, + fromNative = true, + ) + + assertThat(result).isNull() + } + + @Test + fun `resolve goes to pip if its not from native but from the webview`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = true, + fromNative = false, + ) + + assertThat(result).isEqualTo(CallScreenBackPressAction.EnterPictureInPicture) + } + @Test + fun `resolve hangs up if its not from native but from the webview and pip is not supported`() { + val result = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = true, + fromNative = false, + ) + + assertThat(result).isNull() + } + + @Test + fun `invalid cases (event comes from webview but there is now webview) all result in hangup`() { + val withPipSupport = CallScreenBackPressPolicy.resolve( + supportPip = true, + hasWebView = false, + fromNative = false, + ) + assertThat(withPipSupport).isNull() + val withOutPipSupport = CallScreenBackPressPolicy.resolve( + supportPip = false, + hasWebView = false, + fromNative = false, + ) + assertThat(withOutPipSupport).isNull() + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index b6b0120451..276e6670f1 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -13,8 +13,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.MobileScreen -import io.element.android.features.call.api.CallType -import io.element.android.features.call.impl.ui.CallScreenEvents +import io.element.android.features.call.api.CallData +import io.element.android.features.call.impl.ui.CallScreenEvent import io.element.android.features.call.impl.ui.CallScreenNavigator import io.element.android.features.call.impl.ui.CallScreenPresenter import io.element.android.features.call.impl.utils.WidgetMessageSerializer @@ -39,6 +39,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock 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.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin @@ -59,46 +60,19 @@ class CallScreenPresenterTest { val warmUpRule = WarmUpRule() @Test - fun `present - with CallType ExternalUrl just loads the URL and sets the call as active`() = runTest { - val analyticsLambda = lambdaRecorder {} - val joinedCallLambda = lambdaRecorder {} - val presenter = createCallScreenPresenter( - callType = CallType.ExternalUrl("https://call.element.io"), - screenTracker = FakeScreenTracker(analyticsLambda), - activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Wait until the URL is loaded - advanceTimeBy(1.seconds) - skipItems(2) - val initialState = awaitItem() - assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) - assertThat(initialState.webViewError).isNull() - assertThat(initialState.isInWidgetMode).isFalse() - assertThat(initialState.isCallActive).isFalse() - analyticsLambda.assertions().isNeverCalled() - joinedCallLambda.assertions().isCalledOnce() - } - } - - @Test - fun `present - with CallType RoomCall sets call as active, loads URL and runs WidgetDriver`() = runTest { + fun `present - with CallData sets call as active, loads URL and runs WidgetDriver`() = runTest { val widgetDriver = FakeMatrixWidgetDriver() val widgetProvider = FakeCallWidgetProvider(widgetDriver) val analyticsLambda = lambdaRecorder {} - val joinedCallLambda = lambdaRecorder {} + val joinedCallLambda = lambdaRecorder {} val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, widgetProvider = widgetProvider, screenTracker = FakeScreenTracker(analyticsLambda), activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda), ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Wait until the URL is loaded advanceTimeBy(1.seconds) skipItems(1) @@ -107,7 +81,6 @@ class CallScreenPresenterTest { val initialState = awaitItem() assertThat(initialState.urlState).isInstanceOf(AsyncData.Loading::class.java) assertThat(initialState.isCallActive).isFalse() - assertThat(initialState.isInWidgetMode).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetDriver.runCalledCount).isEqualTo(1) analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall)) @@ -123,19 +96,17 @@ class CallScreenPresenterTest { fun `present - set message interceptor, send and receive messages`() = runTest { val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) val initialState = awaitItem() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) // And incoming message from the Widget Driver is passed to the WebView widgetDriver.givenIncomingMessage("A message") @@ -154,24 +125,22 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) - initialState.eventSink(CallScreenEvents.Hangup) + initialState.eventSink(CallScreenEvent.Hangup) // Let background coroutines run and the widget drive be received runCurrent() @@ -188,22 +157,20 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage("""{"action":"io.element.close","api":"fromWidget","widgetId":"1","requestId":"1"}""") @@ -223,22 +190,20 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage( """ { @@ -260,22 +225,20 @@ class CallScreenPresenterTest { val navigator = FakeCallScreenNavigator() val widgetDriver = FakeMatrixWidgetDriver() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), screenTracker = FakeScreenTracker {}, ) val messageInterceptor = FakeWidgetMessageInterceptor() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Give it time to load the URL and WidgetDriver advanceTimeBy(1.seconds) skipItems(2) val initialState = awaitItem() assertThat(initialState.isCallActive).isFalse() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvent.SetupMessageChannels(messageInterceptor)) skipItems(2) // Wait for the timeout to trigger @@ -300,7 +263,7 @@ class CallScreenPresenterTest { val matrixClient = FakeMatrixClient(syncService = syncService) val appForegroundStateService = FakeAppForegroundStateService() val presenter = createCallScreenPresenter( - callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false), + callData = CallData(A_SESSION_ID, A_ROOM_ID, false), widgetDriver = widgetDriver, navigator = navigator, dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), @@ -338,53 +301,8 @@ class CallScreenPresenterTest { } } - @Test - fun `present - error from WebView are updating the state`() = runTest { - val presenter = createCallScreenPresenter( - callType = CallType.ExternalUrl("https://call.element.io"), - activeCallManager = FakeActiveCallManager(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Wait until the URL is loaded - advanceTimeBy(1.seconds) - skipItems(2) - val initialState = awaitItem() - initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) - val finalState = awaitItem() - assertThat(finalState.webViewError).isEqualTo("A Webview error") - } - } - - @Test - fun `present - error from WebView are ignored if Element Call is loaded`() = runTest { - val presenter = createCallScreenPresenter( - callType = CallType.ExternalUrl("https://call.element.io"), - activeCallManager = FakeActiveCallManager(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - // Wait until the URL is loaded - skipItems(1) - val initialState = awaitItem() - - val messageInterceptor = FakeWidgetMessageInterceptor() - initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) - // Emit a message - messageInterceptor.givenInterceptedMessage("A message") - // WebView emits an error, but it will be ignored - initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error")) - val finalState = awaitItem() - assertThat(finalState.webViewError).isNull() - - cancelAndIgnoreRemainingEvents() - } - } - private fun TestScope.createCallScreenPresenter( - callType: CallType, + callData: CallData, navigator: CallScreenNavigator = FakeCallScreenNavigator(), widgetDriver: FakeMatrixWidgetDriver = FakeMatrixWidgetDriver(), widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), @@ -401,7 +319,7 @@ class CallScreenPresenterTest { } val clock = SystemClock { 0 } return CallScreenPresenter( - callType = callType, + callData = callData, navigator = navigator, callWidgetProvider = widgetProvider, userAgentProvider = userAgentProvider, diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.kt new file mode 100644 index 0000000000..76ce8e8ede --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenViewTest.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.call.ui + +import android.view.KeyEvent +import android.webkit.WebView +import androidx.activity.ComponentActivity +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.features.call.impl.pip.PictureInPictureEvent +import io.element.android.features.call.impl.pip.PictureInPictureState +import io.element.android.features.call.impl.pip.aPictureInPictureState +import io.element.android.features.call.impl.ui.CallScreenEvent +import io.element.android.features.call.impl.ui.CallScreenState +import io.element.android.features.call.impl.ui.CallScreenView +import io.element.android.features.call.impl.ui.JavascriptBackHandlerBridge +import io.element.android.features.call.impl.ui.aCallScreenState +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Assert.assertEquals +import org.junit.Test +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import org.robolectric.annotation.Resetter +import org.robolectric.shadows.ShadowWebView + +@OptIn(ExperimentalTestApi::class) +class CallScreenViewTest : RobolectricTest() { + @Test + fun `pressing back key triggers hangup when no web view is available and pip is unsupported`() = runAndroidComposeUiTest { + val callEvents = EventsRecorder() + + setCallScreenView( + state = aCallScreenState(eventSink = callEvents), + useInspectionMode = true, + ) + + pressBackKey() + + callEvents.assertEmpty() + } + + @Config(shadows = [RecordingShadowWebView::class]) + @Test + fun `pressing back key dispatches escape key events to web view when pip is unsupported`() = runAndroidComposeUiTest { + setCallScreenView( + state = aCallScreenState(), + useInspectionMode = false, + pipState = aPictureInPictureState(supportPip = false), + ) + + pressBackKey() + + val dispatchedEvents = RecordingShadowWebView.dispatchedEvents + assertEquals(2, dispatchedEvents.size) + assertEquals(KeyEvent.ACTION_DOWN, dispatchedEvents[0].action) + assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[0].keyCode) + assertEquals(KeyEvent.ACTION_UP, dispatchedEvents[1].action) + assertEquals(KeyEvent.KEYCODE_ESCAPE, dispatchedEvents[1].keyCode) + } + + @Config(shadows = [RecordingShadowWebView::class]) + @Test + fun `web view javascript back handler emits pip event when pip is supported`() = runAndroidComposeUiTest { + val pipEvents = EventsRecorder() + + setCallScreenView( + state = aCallScreenState(), + useInspectionMode = false, + pipState = aPictureInPictureState( + supportPip = true, + eventSink = pipEvents, + ), + ) + + runOnIdle { + RecordingShadowWebView.invokeJavascriptBackHandler() + } + + pipEvents.assertSize(2) + pipEvents.assertTrue(0) { it is PictureInPictureEvent.SetPipController } + pipEvents.assertTrue(1) { it is PictureInPictureEvent.EnterPictureInPicture } + } +} + +@OptIn(ExperimentalTestApi::class) +private fun AndroidComposeUiTest.setCallScreenView( + state: CallScreenState, + useInspectionMode: Boolean, + pipState: PictureInPictureState = aPictureInPictureState(supportPip = false), +) { + setContent { + // Inspection mode disables AndroidView creation; keep it configurable per test. + CompositionLocalProvider(LocalInspectionMode provides useInspectionMode) { + CallScreenView( + state = state, + pipState = pipState, + onConsoleMessage = {}, + requestPermissions = { _, _ -> }, + ) + } + } +} + +@Implements(WebView::class) +internal class RecordingShadowWebView : ShadowWebView() { + companion object { + val dispatchedEvents = mutableListOf() + private var backHandlerJavascriptInterface: JavascriptBackHandlerBridge? = null + + @Resetter + @JvmStatic + @Suppress("unused") + fun resetRecordedEvents() { + dispatchedEvents.clear() + backHandlerJavascriptInterface = null + } + + fun invokeJavascriptBackHandler() { + val backHandler = checkNotNull(backHandlerJavascriptInterface) { "Expected backHandler JavaScript interface to be registered" } + backHandler.onBackPressed() + } + } + + @Implementation + protected override fun addJavascriptInterface(`object`: Any, name: String) { + super.addJavascriptInterface(`object`, name) + if (name == "backHandler") { + backHandlerJavascriptInterface = `object` as? JavascriptBackHandlerBridge + } + } + + @Implementation + @Suppress("unused") + fun dispatchKeyEvent(event: KeyEvent): Boolean { + dispatchedEvents += KeyEvent(event) + return false + } +} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt deleted file mode 100644 index c83408bd3b..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallTypeTest.kt +++ /dev/null @@ -1,45 +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.call.ui - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallType -import io.element.android.features.call.impl.ui.getSessionId -import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.test.A_SESSION_ID -import org.junit.Test - -class CallTypeTest { - @Test - fun `getSessionId returns null for ExternalUrl`() { - assertThat(CallType.ExternalUrl("aURL").getSessionId()).isNull() - } - - @Test - fun `getSessionId returns the sessionId for RoomCall`() { - assertThat( - CallType.RoomCall( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - isAudioCall = false, - ).getSessionId() - ).isEqualTo(A_SESSION_ID) - } - - @Test - fun `ExternalUrl stringification does not contain the URL`() { - assertThat(CallType.ExternalUrl("aURL").toString()).isEqualTo("ExternalUrl") - } - - @Test - fun `RoomCall stringification does not contain the URL`() { - assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, false).toString()) - .isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID, isAudioCall=false)") - } -} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt deleted file mode 100644 index 43f7f931f1..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-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.call.utils - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.impl.utils.CallIntentDataParser -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import java.net.URLEncoder - -@RunWith(RobolectricTestRunner::class) -class CallIntentDataParserTest { - private val callIntentDataParser = CallIntentDataParser() - - @Test - fun `a null data returns null`() { - val url: String? = null - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `empty data returns null`() { - doTest("", null) - } - - @Test - fun `invalid data returns null`() { - doTest("!", null) - } - - @Test - fun `data with no scheme returns null`() { - doTest("test", null) - } - - @Test - fun `Element Call http urls returns null`() { - doTest("http://call.element.io", null) - doTest("http://call.element.io/some-actual-call?with=parameters", null) - } - - @Test - fun `Element Call urls with unknown host returns null`() { - // Check valid host first, should not return null - doTest("https://call.element.io", "https://call.element.io#?appPrompt=false&confineToRoom=true") - // Unknown host should return null - doTest("https://unknown.io", null) - doTest("https://call.unknown.io", null) - doTest("https://call.element.com", null) - doTest("https://call.element.io.tld", null) - } - - @Test - fun `Element Call urls will be returned as is`() { - doTest( - url = "https://call.element.io", - expectedResult = "https://call.element.io#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url param gets url extracted`() { - doTest( - url = VALID_CALL_URL_WITH_PARAM, - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `HTTP and HTTPS urls that don't come from EC return null`() { - doTest("http://app.element.io", null) - doTest("https://app.element.io", null) - doTest("http://", null) - doTest("https://", null) - } - - @Test - fun `Element Call url with no url returns null`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?no_url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme with no call host returns null`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "element://no-call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element scheme with no data returns null`() { - val url = "element://call?url=" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `Element Call url with no data returns null`() { - val url = "io.element.call:/?url=" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `element invalid scheme returns null`() { - val embeddedUrl = VALID_CALL_URL_WITH_PARAM - val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "bad.scheme:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isNull() - } - - @Test - fun `Element Call url with url extra param appPrompt gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM&appPrompt=true", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=true", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true" - ) - } - - @Test - fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=true&otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true" - ) - } - - @Test - fun `Element Call url with url extra param confineToRoom gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM&confineToRoom=false", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=false", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false" - ) - } - - @Test - fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=false&otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false" - ) - } - - @Test - fun `Element Call url with url fragment gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#fragment", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url fragment with params gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with url fragment with other params gets url extracted`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with empty fragment`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - @Test - fun `Element Call url with empty fragment query`() { - doTest( - url = "$VALID_CALL_URL_WITH_PARAM#?", - expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" - ) - } - - private fun doTest(url: String, expectedResult: String?) { - // Test direct parsing - assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) - - // Test embedded url, scheme 1 - val encodedUrl = URLEncoder.encode(url, "utf-8") - val urlScheme1 = "element://call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult) - - // Test embedded url, scheme 2 - val urlScheme2 = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) - } - - companion object { - const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters" - const val EXTRA_PARAMS = "appPrompt=false&confineToRoom=true" - } -} 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 f9f6206ec7..c95f8c5088 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 @@ -13,7 +13,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService 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.api.CallData 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 @@ -45,6 +45,7 @@ import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.plantTestTimber +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.mockk.coVerify import io.mockk.mockk import io.mockk.verify @@ -54,12 +55,9 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf -@RunWith(RobolectricTestRunner::class) -class DefaultActiveCallManagerTest { +class DefaultActiveCallManagerTest : RobolectricTest() { private val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.INCOMING_CALL) @OptIn(ExperimentalCoroutinesApi::class) @@ -77,7 +75,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = false, @@ -104,7 +102,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = true, @@ -132,7 +130,7 @@ class DefaultActiveCallManagerTest { manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) assertThat(manager.activeCall.value).isEqualTo(activeCall) - assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2) + assertThat(manager.activeCall.value?.callData?.roomId).isNotEqualTo(A_ROOM_ID_2) advanceTimeBy(1) @@ -178,7 +176,7 @@ class DefaultActiveCallManagerTest { } @Test - fun `hangUpCall - removes existing call if the CallType matches`() = runTest { + fun `hangUpCall - removes existing call if the CallData matches`() = runTest { setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) @@ -188,7 +186,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false)) + manager.hangUpCall(CallData(notificationData.sessionId, notificationData.roomId, false)) assertThat(manager.activeCall.value).isNull() assertThat(manager.activeWakeLock?.isHeld).isFalse() @@ -215,7 +213,7 @@ class DefaultActiveCallManagerTest { val notificationData = aCallNotificationData(roomId = A_ROOM_ID) manager.registerIncomingCall(notificationData) - manager.hangUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false)) + manager.hangUpCall(CallData(notificationData.sessionId, notificationData.roomId, false)) coVerify { room.declineCall(notificationEventId = notificationData.eventId) @@ -242,7 +240,7 @@ class DefaultActiveCallManagerTest { val notificationData = aCallNotificationData(roomId = A_ROOM_ID) // Do not register the incoming call, so the manager doesn't know about it manager.hangUpCall( - callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId, false), + callData = CallData(notificationData.sessionId, notificationData.roomId, false), notificationData = notificationData, ) coVerify { @@ -320,7 +318,7 @@ class DefaultActiveCallManagerTest { } @Test - fun `hangUpCall - does nothing if the CallType doesn't match`() = runTest { + fun `hangUpCall - does nothing if the CallData doesn't match`() = runTest { setupShadowPowerManager() val notificationManagerCompat = mockk(relaxed = true) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) @@ -329,7 +327,13 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() - manager.hangUpCall(CallType.ExternalUrl("https://example.com")) + manager.hangUpCall( + CallData( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID_2, + isAudioCall = true, + ) + ) assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeWakeLock?.isHeld).isTrue() @@ -344,10 +348,10 @@ class DefaultActiveCallManagerTest { val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) assertThat(manager.activeCall.value).isNull() - manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID, true)) + manager.joinedCall(CallData(A_SESSION_ID, A_ROOM_ID, true)) assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, isAudioCall = true, @@ -450,7 +454,7 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isEqualTo( ActiveCall( - callType = CallType.RoomCall( + callData = CallData( sessionId = callNotificationData.sessionId, roomId = callNotificationData.roomId, isAudioCall = false, diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt index 2d0e126ab5..c2c38284a9 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt @@ -8,7 +8,7 @@ package io.element.android.features.call.utils -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.ActiveCallManager @@ -17,8 +17,8 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeActiveCallManager( var registerIncomingCallResult: (CallNotificationData) -> Unit = {}, - var hangUpCallResult: (CallType, CallNotificationData?) -> Unit = { _, _ -> }, - var joinedCallResult: (CallType) -> Unit = {}, + var hangUpCallResult: (CallData, CallNotificationData?) -> Unit = { _, _ -> }, + var joinedCallResult: (CallData) -> Unit = {}, ) : ActiveCallManager { override val activeCall = MutableStateFlow(null) @@ -26,12 +26,12 @@ class FakeActiveCallManager( registerIncomingCallResult(notificationData) } - override suspend fun hangUpCall(callType: CallType, notificationData: CallNotificationData?) = simulateLongTask { - hangUpCallResult(callType, notificationData) + override suspend fun hangUpCall(callData: CallData, notificationData: CallNotificationData?) = simulateLongTask { + hangUpCallResult(callData, notificationData) } - override suspend fun joinedCall(callType: CallType) = simulateLongTask { - joinedCallResult(callType) + override suspend fun joinedCall(callData: CallData) = simulateLongTask { + joinedCallResult(callData) } fun setActiveCall(value: ActiveCall?) { diff --git a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt index fdf3ca566b..13b61feacb 100644 --- a/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt +++ b/features/call/test/src/main/kotlin/io/element/android/features/call/test/FakeElementCallEntryPoint.kt @@ -8,16 +8,16 @@ package io.element.android.features.call.test -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.tests.testutils.lambda.lambdaError class FakeElementCallEntryPoint( - var startCallResult: (CallType) -> Unit = { lambdaError() }, + var startCallResult: (CallData) -> Unit = { lambdaError() }, var handleIncomingCallResult: ( - CallType.RoomCall, + CallData, EventId, UserId, String?, @@ -27,12 +27,12 @@ class FakeElementCallEntryPoint( String?, ) -> Unit = { _, _, _, _, _, _, _, _ -> lambdaError() } ) : ElementCallEntryPoint { - override fun startCall(callType: CallType) { - startCallResult(callType) + override fun startCall(callData: CallData) { + startCallResult(callData) } override suspend fun handleIncomingCall( - callType: CallType.RoomCall, + callData: CallData, eventId: EventId, senderId: UserId, roomName: String?, @@ -44,7 +44,7 @@ class FakeElementCallEntryPoint( textContent: String?, ) { handleIncomingCallResult( - callType, + callData, eventId, senderId, roomName, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 0a7309a2b0..e03d36363d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -13,6 +13,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -36,6 +39,13 @@ fun AddPeopleView( modifier: Modifier = Modifier, invitePeopleView: @Composable () -> Unit, ) { + val currentOnFinish by rememberUpdatedState(onFinish) + LaunchedEffect(state.sendInvitesAction, state.createRoomFromDmAction) { + if (state.sendInvitesAction.isSuccess() || state.createRoomFromDmAction.isSuccess()) { + currentOnFinish() + } + } + HeaderFooterPage( modifier = modifier, contentPadding = PaddingValues(0.dp), @@ -47,7 +57,6 @@ fun AddPeopleView( text = stringResource(CommonStrings.action_finish), onClick = { state.eventSink(InvitePeopleEvents.SendInvites) - onFinish() }, enabled = state.canInvite, modifier = Modifier diff --git a/features/createroom/impl/src/main/res/values-be/translations.xml b/features/createroom/impl/src/main/res/values-be/translations.xml index 9669bb9090..6b254003b7 100644 --- a/features/createroom/impl/src/main/res/values-be/translations.xml +++ b/features/createroom/impl/src/main/res/values-be/translations.xml @@ -3,9 +3,15 @@ "Новы пакой" "Запрасіць карыстальнікаў" "Пры стварэнні пакоя адбылася памылка" + "Дадаць імя…" + "Новы пакой" + "Новая прастора" "Толькі запрошаныя людзі могуць атрымаць доступ да гэтага пакоя. Усе паведамленні абаронены end-to-end шыфраваннем." "Любы можа знайсці гэты пакой. Вы можаце змяніць гэта ў любы час у наладах пакоя." "Папрасіце далучыцца" + "Адрас" + "Дадаць у прастору" "Тэма (неабавязкова)" + "Дадаць апісанне…" diff --git a/features/createroom/impl/src/main/res/values-ca/translations.xml b/features/createroom/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..f9e1dea0fc --- /dev/null +++ b/features/createroom/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,16 @@ + + + "Sala nova" + "Convida persones" + "S\'ha produït un error en crear la sala" + "Només s\'hi poden unir les persones convidades." + "Tothom pot trobar aquesta sala. +Pots canviar-ho en qualsevol moment a la configuració de sala." + "Qualsevol persona pot sol·licitar unir-s\'hi però un administrador o moderador l\'haurà d\'acceptar" + "Permet sol·licituds d\'unió" + "Tothom pot unir-s\'hi." + "És necessària una adreça perquè sigui visible al directori públic." + "Adreça" + "Visibilitat de sala" + "Tema (opcional)" + diff --git a/features/createroom/impl/src/main/res/values-da/translations.xml b/features/createroom/impl/src/main/res/values-da/translations.xml index 66c4f08b70..ab72755dcf 100644 --- a/features/createroom/impl/src/main/res/values-da/translations.xml +++ b/features/createroom/impl/src/main/res/values-da/translations.xml @@ -19,7 +19,7 @@ Du kan ændre dette når som helst i rummets indstillinger." "Anmod om at deltage" "Kun inviterede brugere kan deltage." "Privat" - "Alle kan deltage i dette rum" + "Alle kan deltage." "Offentlig" "Alle i %1$s kan deltage." "Standard" diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml index 4408bf66a3..c3f42ca287 100644 --- a/features/createroom/impl/src/main/res/values-de/translations.xml +++ b/features/createroom/impl/src/main/res/values-de/translations.xml @@ -28,7 +28,8 @@ Du kannst dies jederzeit in den Einstellungen des Chats ändern." "Adresse" " Sichtbarkeit des Chats" "(kein Space)" - "Home" + "Nicht zu einem Space hinzufügen" + "Kein Space ausgewählt" "Space hinzufügen" "Thema (optional)" "Beschreibung hinzufügen…" diff --git a/features/createroom/impl/src/main/res/values-fa/translations.xml b/features/createroom/impl/src/main/res/values-fa/translations.xml index 27542ccc19..821d55af67 100644 --- a/features/createroom/impl/src/main/res/values-fa/translations.xml +++ b/features/createroom/impl/src/main/res/values-fa/translations.xml @@ -3,7 +3,7 @@ "اتاق جدید" "دعوت افراد" "هنگام ایجاد اتاق خطایی رخ داد" - "تنها افراد دعوت شده می‌توانند به این اتاق دسترسی داشته باشند. همهٔ پیام‌ها رمزنگاری سرتاسری شده‌اند." + "تنها افراد دعوت شده می‌توانند بپیوندند." "هرکسی می‌تواند اتاق را بیابد. می‌توانید بعداً در تظیمات اتاق عوضش کنید." "درخواست دعوت" diff --git a/features/createroom/impl/src/main/res/values-pl/translations.xml b/features/createroom/impl/src/main/res/values-pl/translations.xml index 0164225ea2..b602cb13d8 100644 --- a/features/createroom/impl/src/main/res/values-pl/translations.xml +++ b/features/createroom/impl/src/main/res/values-pl/translations.xml @@ -3,14 +3,34 @@ "Nowy pokój" "Zaproś znajomych" "Wystąpił błąd w trakcie tworzenia pokoju" - "Tylko zaproszone osoby mogą dołączyć do tego pokoju. Wszystkie wiadomości są szyfrowane end-to-end." + "Nie udało się utworzyć przestrzeni z powodu nieznanego błędu. Spróbuj ponownie później." + "Dodaj nazwę…" + "Nowy pokój" + "Nowa przestrzeń" + "Dołączyć mogą tylko zaproszone osoby." + "Prywatny" "Każdy może znaleźć ten pokój. Możesz to zmienić w ustawieniach pokoju." - "Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić prośbę" - "Poproś o dołączenie" - "Każdy może dołączyć do tego pokoju" - "Aby ten pokój był widoczny w katalogu pomieszczeń publicznych, będziesz potrzebował adres pokoju." - "Adres pokoju" + "Każdy może dołączyć." + "Publiczny" + "Każdy może poprosić o dołączenie, ale administrator lub moderator musi to zaakceptować." + "Zezwól na prośbę o dołączenie" + "Każdy w %1$s może dołączyć, ale wszyscy pozostali muszą poprosić o dostęp." + "Poproś o dołączenie" + "Dołączyć mogą tylko zaproszone osoby." + "Prywatny" + "Każdy może dołączyć." + "Publiczny" + "Każdy w %1$s może dołączyć." + "Standardowy" + "Kto ma dostęp" + "Aby ten pokój był widoczny w katalogu pomieszczeń publicznych, potrzebny jest adres pokoju." + "Adres" "Widoczność pomieszczenia" + "(brak przestrzeni)" + "Nie dodawaj do przestrzeni" + "Nie wybrano przestrzeni" + "Dodaj do przestrzeni" "Temat (opcjonalnie)" + "Dodaj opis…" diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index fd1854db40..33a5351877 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,14 +3,34 @@ "Cameră nouă" "Invitați prieteni" "A apărut o eroare la crearea camerei" + "Spațiul nu a putut fi creat din cauza unei erori necunoscute. Încercați din nou mai târziu." + "Adăugați un nume…" + "Cameră nouă" + "Spațiu nou" "Doar persoanele invitate se pot alătura." + "Privat" "Oricine poate găsi această cameră. Puteți modifica acest lucru oricând în setări." + "Oricine se poate alătura." + "Public" "Oricine poate cere să se alăture camerei, dar un administrator sau un moderator va trebui să accepte cererea" "Permite solicitarea de alăturare" + "Oricine din %1$s se poate alătura, dar oricine altcineva trebuie să solicite acces." + "Solicitați să vă alăturați" + "Doar persoanele invitate se pot alătura." + "Privat" "Oricine se poate alătura acestei camere" + "Public" + "Oricine din %1$s se poate alătura." + "Standard" + "Cine are acces" "Pentru ca această cameră să fie vizibilă în directorul de camere publice, veți avea nevoie de o adresă de cameră." "Adresă" "Vizibilitatea camerei" + "(nicun spațiu)" + "Nu adăugați la un spațiu" + "Niciun spațiu selectat" + "Adăugați la spațiu" "Subiect (opțional)" + "Adăugați o descriere…" diff --git a/features/createroom/impl/src/main/res/values-uk/translations.xml b/features/createroom/impl/src/main/res/values-uk/translations.xml index d01da0dc35..a40a2b21a5 100644 --- a/features/createroom/impl/src/main/res/values-uk/translations.xml +++ b/features/createroom/impl/src/main/res/values-uk/translations.xml @@ -3,22 +3,32 @@ "Нова кімната" "Запросити людей" "Під час створення кімнати сталася помилка" + "Простір не вдалося створити через невідому помилку. Спробуйте ще раз пізніше." "Додати назву…" "Нова кімната" "Новий простір" "Можуть приєднатися лише запрошені люди." + "Приватний" "Будь-хто може знайти цю кімнату. Ви можете змінити це в будь-який час у налаштуваннях кімнати." "Приєднатися може будь-хто." + "Публічний" "Будь-хто може подати запит на приєднання, але адміністратор або модератор повинен схвалити запит." "Дозволити запит на приєднання" + "Будь-хто з %1$s може приєднатися, але всі інші повинні подати запит на доступ." + "Запит на приєднання" "Приєднатися можуть лише запрошені особи." + "Приватний" "Приєднатися може будь-хто." + "Публічний" "Приєднатися може будь-хто з %1$s." + "Стандартний" "Хто має доступ" "Вам знадобиться адреса, щоб зробити її видимою в загальнодоступному каталозі." "Адреса" "Видимість кімнати" + "(без пробілу)" + "Не додавати до простору" "Головна" "Додати до простору" "Тема (необов\'язково)" diff --git a/features/createroom/impl/src/main/res/values-zh/translations.xml b/features/createroom/impl/src/main/res/values-zh/translations.xml index 8d898fe5f0..1ba1036634 100644 --- a/features/createroom/impl/src/main/res/values-zh/translations.xml +++ b/features/createroom/impl/src/main/res/values-zh/translations.xml @@ -1,13 +1,13 @@ "新房间" - "邀请朋友" + "邀请人员" "创建房间时出错" "由于未知错误,空间创建失败。请稍后再试。" "添加名称…" "新房间" "新空间" - "仅限受邀者加入。" + "仅限受邀人员加入。" "私密" "任何人都能找到此房间。 你可以随时在房间设置中更改。" @@ -17,11 +17,11 @@ "申请加入" "%1$s 中的任何人都可以加入,但其他人必须申请访问。" "申请加入" - "仅限受邀者加入。" + "仅限受邀人员加入。" "私密" "任何人都可以加入。" "公共" - "%1$s 中的任何人可加入。" + "%1$s 中的任何人都可以加入。" "标准" "谁有权访问此房间" "要使该房间在公共目录中可见,你需要一个地址。" diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt index fcedcb2367..c0c4a8a5fe 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt @@ -56,6 +56,7 @@ import io.element.android.services.analytics.api.AnalyticsService 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.robolectric.RobolectricTest import io.element.android.tests.testutils.test import io.mockk.mockk import kotlinx.collections.immutable.persistentMapOf @@ -66,8 +67,6 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import java.io.File import java.util.Optional @@ -75,8 +74,7 @@ private const val AN_URI_FROM_CAMERA = "content://uri_from_camera" private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2" private const val AN_URI_FROM_GALLERY = "content://uri_from_gallery" -@RunWith(RobolectricTestRunner::class) -class ConfigureRoomPresenterTest { +class ConfigureRoomPresenterTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() @@ -301,7 +299,9 @@ class ConfigureRoomPresenterTest { roomName = 0, roomAvatar = 0, roomTopic = 0, - spaceChild = 0 + spaceChild = 0, + beacon = 0, + beaconInfo = 0, ), users = persistentMapOf(), ) diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt index 352efdfb92..c942d2df72 100644 --- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt +++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt @@ -8,7 +8,6 @@ package io.element.android.features.logout.impl -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -60,6 +59,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart 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.PasswordVisibilityToggle 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.TextField @@ -280,14 +280,10 @@ private fun Content( placeholder = stringResource(CommonStrings.common_password), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { - val image = - if (passwordVisible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() - val description = - if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) - - Box(modifier = Modifier.clickable { passwordVisible = !passwordVisible }) { - Icon(imageVector = image, description) - } + PasswordVisibilityToggle( + visible = passwordVisible, + onToggle = { passwordVisible = !passwordVisible }, + ) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, diff --git a/features/deactivation/impl/src/main/res/values-ca/translations.xml b/features/deactivation/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..c8ad39f7ae --- /dev/null +++ b/features/deactivation/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,13 @@ + + + "Si us plau, confirma que vols desactivar el teu compte. Aquesta acció no es pot desfer." + "Elimina tots els meus missatges" + "Avís: els futurs usuaris podrien veure converses incompletes." + "La desactivació del compte és %1$s, implica:" + "irreversible" + "%1$s el compte (no podràs tornar a iniciar sessió i el teu ID no es podrà reutilitzar)." + "Desactiva permanentment" + "Se t\'eliminarà de totes les sales o xats." + "S\'eliminarà la informació del compte del nostre servidor d\'identitat." + "Els teus missatges continuaran sent visibles per als usuaris registrats, però no estaran disponibles per a usuaris nous o no registrats si decideixes eliminar-los." + diff --git a/features/deactivation/impl/src/main/res/values-cs/translations.xml b/features/deactivation/impl/src/main/res/values-cs/translations.xml index e0f4fd14c7..13659e1a75 100644 --- a/features/deactivation/impl/src/main/res/values-cs/translations.xml +++ b/features/deactivation/impl/src/main/res/values-cs/translations.xml @@ -1,14 +1,14 @@ - "Potvrďte prosím, že chcete svůj účet deaktivovat. Tuto akci nelze vrátit zpět." + "Potvrďte prosím, že chcete smazat svůj účet. Tuto akci nelze vrátit zpět." "Smazat všechny mé zprávy" "Upozornění: Budoucí uživatelé mohou vidět neúplné konverzace." - "Deaktivace vašeho účtu je %1$s, což způsobí:" + "Smazání účtu je %1$s, dojde k:" "nezvratná" "%1$s váš účet (nemůžete se znovu přihlásit a vaše ID nelze znovu použít)." "Trvale zakázat" "Odebere vás ze všech chatovacích místností." "Odstraní informace o vašem účtu z našeho serveru identit." "Vaše zprávy budou stále viditelné registrovaným uživatelům, ale nebudou dostupné novým ani neregistrovaným uživatelům, pokud se rozhodnete je smazat." - "Deaktivovat účet" + "Smazat účet" diff --git a/features/deactivation/impl/src/main/res/values-da/translations.xml b/features/deactivation/impl/src/main/res/values-da/translations.xml index dfbf00a1c6..f434547b8a 100644 --- a/features/deactivation/impl/src/main/res/values-da/translations.xml +++ b/features/deactivation/impl/src/main/res/values-da/translations.xml @@ -1,13 +1,14 @@ - "Bekræft venligst, at du vil deaktivere din konto. Denne handling kan ikke fortrydes." + "Bekræft venligst, at du ønsker at slette din konto. Denne handling kan ikke fortrydes." "Slet alle mine beskeder" "Advarsel: Fremtidige brugere kan muligvis se ufuldstændige samtaler." - "Deaktivering af din konto er %1$s, det vil:" + "Sletning af din konto er %1$s, det vil:" "irreversibel" "%1$s din konto (du kan ikke logge ind igen, og dit ID kan ikke genbruges)." "Permanent deaktivere" "Fjerne dig fra alle samtaler" "Slette dine kontooplysninger fra vores identitetsserver." "Dine beskeder vil stadig være synlige for registrerede brugere, men vil ikke være tilgængelige for nye eller uregistrerede brugere, hvis du vælger at slette dem." + "Slet konto" diff --git a/features/deactivation/impl/src/main/res/values-de/translations.xml b/features/deactivation/impl/src/main/res/values-de/translations.xml index e03d53bbce..8430134d00 100644 --- a/features/deactivation/impl/src/main/res/values-de/translations.xml +++ b/features/deactivation/impl/src/main/res/values-de/translations.xml @@ -1,14 +1,14 @@ - "Bitte bestätige, dass du dein Konto deaktivieren möchtest. Dies kann nicht rückgängig gemacht werden." + "Bitte bestätige, dass du dein Konto löschen möchtest. Diese Aktion kann nicht rückgängig gemacht werden." "Lösche alle meine Nachrichten" "Warnung: Künftigen Nutzern werden möglicherweise unvollständige Konversationen angezeigt." - "Dein Konto zu deaktivieren ist %1$s. Folgendes wird passieren:" + "Das Löschen deines Kontos ist %1$s. Es wird:" "irreversibel" "%1$s dein Konto (du kannst dich nicht erneut anmelden und deine ID kann nicht wiederverwendet werden)." "Dauerhaft deaktivieren" "Du wirst aus allen Chats entfernt." "Lösche deine Kontoinformationen von unserem Identitätsserver." "Deine Nachrichten werden für bereits registrierte Nutzer weiterhin sichtbar sein. Für neue oder unregistrierte Nutzer sind sie nicht verfügbar, wenn du sie löschen solltest." - "Benutzerkonto deaktivieren" + "Konto löschen" diff --git a/features/deactivation/impl/src/main/res/values-el/translations.xml b/features/deactivation/impl/src/main/res/values-el/translations.xml index ac645f3063..b6a359abbf 100644 --- a/features/deactivation/impl/src/main/res/values-el/translations.xml +++ b/features/deactivation/impl/src/main/res/values-el/translations.xml @@ -1,14 +1,14 @@ - "Παρακαλώ επιβεβαίωσε ότι θες να απενεργοποιήσεις τον λογαριασμό σου. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί." + "Επιβεβαιώστε ότι θέλετε να διαγράψετε τον λογαριασμό σας. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί." "Διαγραφή όλων των μηνυμάτων μου" "Προειδοποίηση: Οι μελλοντικοί χρήστες ενδέχεται να βλέπουν ελλιπείς συνομιλίες." - "Η απενεργοποίηση του λογαριασμού σας είναι %1$s, θα:" + "Η διαγραφή του λογαριασμού σας είναι %1$s, και θα:" "μη αναστρέψιμο" "%1$s τον λογαριασμό σου (δεν μπορείς να συνδεθείς ξανά και το αναγνωριστικό σου δεν μπορεί να επαναχρησιμοποιηθεί)." "Μόνιμη απενεργοποίηση" "Αποχώρησή σας από όλες τις αίθουσες συνομιλίας." "Διαγράψει τα στοιχεία του λογαριασμού σου από τον διακομιστή ταυτότητάς μας." "Τα μηνύματά σου θα εξακολουθούν να είναι ορατά στους εγγεγραμμένους χρήστες, αλλά δεν θα είναι διαθέσιμα σε νέους ή μη εγγεγραμμένους χρήστες εάν επιλέξεις να τα διαγράψεις." - "Απενεργοποίηση λογαριασμού" + "Διαγραφή λογαριασμού" diff --git a/features/deactivation/impl/src/main/res/values-et/translations.xml b/features/deactivation/impl/src/main/res/values-et/translations.xml index 95695fef16..bfb41d7665 100644 --- a/features/deactivation/impl/src/main/res/values-et/translations.xml +++ b/features/deactivation/impl/src/main/res/values-et/translations.xml @@ -1,14 +1,14 @@ - "Palun kinnita uuesti, et soovid eemaldada oma konto kasutusest" + "Palun kinnita uuesti, et soovid kustutada oma kasutajakonto. Seda tegevust ei saa tagasi pöörata." "Kustuta kõik minu sõnumid" "Hoiatus: tulevased kasutajad võivad näha poolikuid vestlusi." - "Sinu konto kasutusest eemaldamine on %1$s ja sellega:" + "Sinu konto kustutamine on %1$s ja sellega:" "pöördumatu" "Sinu kasutajakonto %1$s (sa ei saa enam sellega võrku logida ning kasutajatunnust ei saa enam pruukida)." "jäädavalt eemaldatakse kasutusest" "Sind logitakse välja kõikidest jututubadest." "Kustutatakse sinu andmed meie isikutuvastusserverist." "Sinu sõnumid on jätkuvalt nähtavad registreeritud kasutajatele, kuid kui otsustad sõnumid kustutada, siis nad nad pole nähtavad uutele ja registreerimata kasutajatele." - "Eemalda konto kasutusest" + "Kustuta kasutajakonto" diff --git a/features/deactivation/impl/src/main/res/values-fi/translations.xml b/features/deactivation/impl/src/main/res/values-fi/translations.xml index df2543be70..148fe3d610 100644 --- a/features/deactivation/impl/src/main/res/values-fi/translations.xml +++ b/features/deactivation/impl/src/main/res/values-fi/translations.xml @@ -1,14 +1,14 @@ - "Vahvista, että haluat deaktivoida tilisi. Tätä ei voi perua." + "Vahvista, että haluat poistaa tilisi. Tätä ei voi perua." "Poista kaikki viestini" "Varoitus: Tulevaisuudessa muut voivat nähdä puutteellisia keskusteluja." - "Tilisi deaktivointia %1$s. Jos teet sen:" + "Tilisi poistamista %1$s. Jos teet sen:" "ei voi peruuttaa" "Tilisi %1$s (et voi kirjautua takaisin sisään, eikä tunnustasi voi käyttää uudelleen)." "poistetaan käytöstä pysyvästi" "Sinut poistetaan kaikista keskusteluhuoneista." "Tilitietosi poistetaan identiteettipalvelimeltamme." "Viestisi näkyvät edelleen rekisteröityneille käyttäjille, mutta ne eivät ole uusien tai rekisteröimättömien käyttäjien saatavilla, jos päätät poistaa ne." - "Deaktivoi tili" + "Poista tili" diff --git a/features/deactivation/impl/src/main/res/values-fr/translations.xml b/features/deactivation/impl/src/main/res/values-fr/translations.xml index 875142bc01..cf69cb3275 100644 --- a/features/deactivation/impl/src/main/res/values-fr/translations.xml +++ b/features/deactivation/impl/src/main/res/values-fr/translations.xml @@ -1,14 +1,14 @@ - "Veuillez confirmer que vous souhaitez désactiver votre compte. Cette action ne peut pas être annulée." + "Veuillez confirmer que vous souhaitez supprimer votre compte. Cette action ne peut pas être annulée." "Supprimer tous mes messages" "Attention : les futurs utilisateurs pourraient voir des conversations incomplètes." - "La désactivation de votre compte est %1$s, cela va :" + "La suppression de votre compte est %1$s, cela va :" "irréversible" "%1$s votre compte (vous ne pourrez plus vous reconnecter et votre identifiant ne pourra pas être réutilisé)." "Désactiver définitivement" "Vous retirer de tous les salons et toutes les discussions." "Supprimer les informations de votre compte du serveur d’identité." "Rendre vos messages invisibles aux futurs membres des salons si vous choisissez de les supprimer. Vos messages seront toujours visibles pour les utilisateurs qui les ont déjà récupérés." - "Désactiver votre compte" + "Supprimer le compte" diff --git a/features/deactivation/impl/src/main/res/values-hr/translations.xml b/features/deactivation/impl/src/main/res/values-hr/translations.xml index 9273254886..1d8a02a08c 100644 --- a/features/deactivation/impl/src/main/res/values-hr/translations.xml +++ b/features/deactivation/impl/src/main/res/values-hr/translations.xml @@ -10,4 +10,5 @@ "Ukloniti vas iz svih soba za razgovore." "Izbrisati podatke o vašem računu s našeg poslužitelja identiteta." "Vaše će poruke i dalje biti vidljive registriranim korisnicima, ali neće biti dostupne novim ili neregistriranim korisnicima ako ih odlučite izbrisati." + "Izbriši račun" diff --git a/features/deactivation/impl/src/main/res/values-hu/translations.xml b/features/deactivation/impl/src/main/res/values-hu/translations.xml index 3d3722b8ef..2c3f51ed7a 100644 --- a/features/deactivation/impl/src/main/res/values-hu/translations.xml +++ b/features/deactivation/impl/src/main/res/values-hu/translations.xml @@ -1,14 +1,14 @@ - "Erősítse meg, hogy deaktiválja a fiókját. Ez a művelet nem vonható vissza." + "Erősítse meg a fiókja törlését. Ez a művelet nem vonható vissza." "Összes saját üzenet törlése" "Figyelmeztetés: A jövőbeli felhasználók hiányos beszélgetéseket láthatnak." - "A fiók deaktiválása %1$s, a következőket okozza:" + "Fiókjának törlése: %1$s, ez a következőket eredményezi:" "visszafordíthatatlan" "%1$s a fiókját (nem fog tudni újra bejelentkezni, és az azonosítója nem használható újra)." "Véglegesen letiltja" "Eltávolításra kerül az összes csevegőszobából." "Törlésre kerülnek a fiókadatai az azonosítási kiszolgálónkról." "Üzenetei továbbra is láthatóak maradnak a regisztrált felhasználók számára, de nem lesznek elérhetőek az új vagy nem regisztrált felhasználók számára, ha úgy dönt, hogy törli őket." - "Fiók deaktiválása" + "Fiók törlése" diff --git a/features/deactivation/impl/src/main/res/values-it/translations.xml b/features/deactivation/impl/src/main/res/values-it/translations.xml index 7cd484d0e4..e3de1ec8bb 100644 --- a/features/deactivation/impl/src/main/res/values-it/translations.xml +++ b/features/deactivation/impl/src/main/res/values-it/translations.xml @@ -1,14 +1,14 @@ - "Conferma di voler disattivare il tuo account. Questa azione è irreversibile." + "Conferma di voler eliminare il tuo account. Questa azione è irreversibile." "Elimina tutti i miei messaggi" "Attenzione: gli utenti futuri potrebbero vedere conversazioni incomplete." - "La disattivazione del tuo account è %1$s , quindi:" + "L\'eliminazione del tuo account è %1$s, e comporterà:" "irreversibile" "%1$s il tuo account (non puoi riaccedere e il tuo ID non può essere riutilizzato)." "Disattiva permanentemente" "Ti rimuove da tutte le stanze di chat." "Elimina le informazioni del tuo account dal nostro server di identità." "I tuoi messaggi saranno ancora visibili agli utenti registrati, ma non saranno disponibili per gli utenti nuovi o non registrati se decidi di eliminarli." - "Disattivazione dell\'account" + "Elimina account" diff --git a/features/deactivation/impl/src/main/res/values-ja/translations.xml b/features/deactivation/impl/src/main/res/values-ja/translations.xml index f41dd1d282..53893a594f 100644 --- a/features/deactivation/impl/src/main/res/values-ja/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ja/translations.xml @@ -1,9 +1,9 @@ - "アカウントを無効化することを再度確認します。この操作は元に戻せません。" + "アカウントを削除しようとしていることを確認しています。この操作は元に戻せません。" "メッセージをすべて削除" "注意: 新しいユーザーには断片的な会話が表示されます" - "アカウントを無効化することは %1$s であり、次の変化が生じます:" + "アカウントを削除することは %1$s であり、次の変化が生じます:" "不可逆" "アカウントを %1$s (再度ログイン不可, 同一のIDを再利用不可)" "恒久的に無効化する" diff --git a/features/deactivation/impl/src/main/res/values-ko/translations.xml b/features/deactivation/impl/src/main/res/values-ko/translations.xml index 42dd0aa0e2..83eaf7ff21 100644 --- a/features/deactivation/impl/src/main/res/values-ko/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ko/translations.xml @@ -3,11 +3,12 @@ "계정을 비활성화하시겠습니까? 이 작업은 되돌릴 수 없습니다." "모든 내 메시지 삭제" "경고: 향후 사용자는 불완전한 대화 내용을 볼 수 있습니다." - "계정을 비활성화하는 것은 %1$s 이며, 다음과 같은 조치를 취합니다:" + "계정을 삭제하면 %1$s, 다음과 같이 처리됩니다:" "불가역적" "%1$s 귀하의 계정 (로그인할 수 없으며, 귀하의 ID는 재사용할 수 없습니다)." "영구적으로 비활성화" "모든 채팅방에서 자신을 제거하세요." "당사의 신원 서버에서 귀하의 계정 정보를 삭제하세요." "메시지는 등록된 사용자에게는 계속 표시되지만, 삭제하면 신규 또는 미등록 사용자는 볼 수 없게 됩니다." + "계정 삭제" diff --git a/features/deactivation/impl/src/main/res/values-pl/translations.xml b/features/deactivation/impl/src/main/res/values-pl/translations.xml index bddb6a9037..5778123aa5 100644 --- a/features/deactivation/impl/src/main/res/values-pl/translations.xml +++ b/features/deactivation/impl/src/main/res/values-pl/translations.xml @@ -1,14 +1,14 @@ - "Potwierdź dezaktywacje konta. Tej akcji nie można cofnąć." + "Potwierdź usunięcie konta. Tej akcji nie można cofnąć." "Usuń wszystkie moje wiadomości" "Ostrzeżenie: Przyszli użytkownicy mogą zobaczyć niekompletne rozmowy." - "Dezaktywacja konta jest %1$s, zostanie:" + "Usunięcie konta jest %1$s, co spowoduje:" "nieodwracalna" "%1$s twoje konto (nie będziesz mógł się zalogować, a twoje ID przepadnie)." "Permanentnie wyłączy" "Usunie Ciebie ze wszystkich pokoi rozmów." "Usunięte wszystkie dane konta z naszego serwera tożsamości." "Twoje wiadomości wciąż będą widoczne dla zarejestrowanych użytkowników, ale nie będą dostępne dla nowych lub niezarejestrowanych użytkowników, jeśli je usuniesz." - "Dezaktywuj konto" + "Usuń konto" diff --git a/features/deactivation/impl/src/main/res/values-ro/translations.xml b/features/deactivation/impl/src/main/res/values-ro/translations.xml index acd4c0747d..6176b4584e 100644 --- a/features/deactivation/impl/src/main/res/values-ro/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ro/translations.xml @@ -1,14 +1,14 @@ - "Vă rugăm să confirmați că doriți să vă dezactivați contul. Această acțiune nu poate fi anulată." + "Vă rugăm să confirmați că doriți să vă ștergeți contul. Această acțiune nu poate fi anulată." "Ștergeți toate mesajele mele" "Avertisment: este posibil ca viitorii utilizatori să vadă conversații incomplete." - "Dezactivarea contului dumneavoastră este %1$s, acesta va:" + "Ștergerea contului dumneavoastră este %1$s, acesta va:" "ireversibilă" "%1$s contul dumneavoastră (nu vă puteți conecta din nou, iar ID-ul dvs. nu poate fi reutilizat)." "Dezactivați permanent" "Îndepărta din toate camerele de chat." "Șterge informațiile contului dumneavoastră de pe serverul nostru de identitate." "Mesajele dumneavoastră vor fi în continuare vizibile pentru utilizatorii înregistrați, dar nu vor fi disponibile pentru utilizatorii noi sau neînregistrați dacă alegeți să le ștergeți." - "Dezactivați contul" + "Ștergeți contul" diff --git a/features/deactivation/impl/src/main/res/values-ru/translations.xml b/features/deactivation/impl/src/main/res/values-ru/translations.xml index 6f595cde29..17a5158f44 100644 --- a/features/deactivation/impl/src/main/res/values-ru/translations.xml +++ b/features/deactivation/impl/src/main/res/values-ru/translations.xml @@ -1,14 +1,14 @@ - "Вы уверены, что хотите отключить свою учётную запись? Данное действие необратимо." + "Вы уверены, что хотите удалить свою учётную запись? Данное действие необратимо." "Удалить все мои сообщения" "Внимание: в будущем пользователи могут видеть неполные переписки." - "Деактивация вашего аккаунта %1$s и означает следующее:" + "Удаление вашего аккаунта %1$s, это означает следующее:" "необратимо" "Ваша учётная запись будет %1$s (вы не сможете войти в неё снова, и другие пользователи не смогут использовать ваше имя пользователя)." "Отключить навсегда" "Вы будете удалены из всех чатов." "Данные Вашего аккаунта будут удалены с нашего сервера идентификации." - "Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если вы решите удалить их." - "Отключить учётную запись" + "Ваши сообщения по-прежнему будут видны зарегистрированным пользователям, но не будут доступны новым или незарегистрированным пользователям, если Вы решите удалить их." + "Удалить аккаунт" diff --git a/features/deactivation/impl/src/main/res/values-uz/translations.xml b/features/deactivation/impl/src/main/res/values-uz/translations.xml index d357f38186..e0dcfe59ef 100644 --- a/features/deactivation/impl/src/main/res/values-uz/translations.xml +++ b/features/deactivation/impl/src/main/res/values-uz/translations.xml @@ -10,4 +10,5 @@ "Sizni barcha chat xonalaridan olib tashlash." "Hisobingiz haqidagi axborotni identifikatsiya serverimizdan o‘chirib tashlang." "Xabarlaringiz ro‘yxatdan o‘tgan foydalanuvchilarga ko‘rinadi, lekin ularni o‘chirishni tanlasangiz, yangi yoki ro‘yxatdan o‘tmagan foydalanuvchilarga ko‘rinmaydi." + "Akkauntni o‘chirish" diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt index 26c942da1f..caf4c0384f 100644 --- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt +++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.logout.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.deactivation.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -26,33 +28,28 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressTag -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class AccountDeactivationViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class AccountDeactivationViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState(eventSink = eventsRecorder), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Deactivate emits the expected Event`() { + fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -60,14 +57,14 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_delete) + clickOn(CommonStrings.action_delete) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) } @Test - fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() { + fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -76,14 +73,14 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false)) } @Test - fun `clicking on retry on the confirmation dialog emits the expected Event`() { + fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -92,26 +89,26 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true)) } @Test - fun `switching on the erase all switch emits the expected Event`() { + fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) + clickOn(R.string.screen_deactivate_account_delete_all_messages) eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true)) } @Test - fun `switching off the erase all switch emits the expected Event`() { + fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( eraseData = true, @@ -119,15 +116,15 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_deactivate_account_delete_all_messages) + clickOn(R.string.screen_deactivate_account_delete_all_messages) eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false)) } @Config(qualifiers = "h1024dp") @Test - fun `typing text in the password field emits the expected Event`() { + fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAccountDeactivationView( + setAccountDeactivationView( state = anAccountDeactivationState( deactivateFormState = aDeactivateFormState( password = A_PASSWORD, @@ -135,12 +132,12 @@ class AccountDeactivationViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") + onNodeWithTag(TestTags.loginPassword.value).performTextInput("A") eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD")) } } -private fun AndroidComposeTestRule.setAccountDeactivationView( +private fun AndroidComposeUiTest.setAccountDeactivationView( state: AccountDeactivationState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 65fe3fe087..92d8b9b646 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow interface EnterpriseService { val isEnterpriseBuild: Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean + suspend fun tweakMasUrl(url: String, homeserver: String): String fun defaultHomeserverList(): List suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt index 6bd6c78de5..f87dc743e8 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt @@ -10,6 +10,7 @@ package io.element.android.features.enterprise.api interface SessionEnterpriseService { suspend fun isElementCallAvailable(): Boolean + suspend fun tweakMasUrl(url: String): String suspend fun init() } diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 932d082fd9..6e3ed5d3cc 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -23,7 +23,7 @@ class DefaultEnterpriseService : EnterpriseService { override val isEnterpriseBuild = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false - + override suspend fun tweakMasUrl(url: String, homeserver: String) = url override fun defaultHomeserverList(): List = emptyList() override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt index 3441063a8a..9aafcd343c 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt @@ -15,5 +15,6 @@ import io.element.android.libraries.di.SessionScope @ContributesBinding(SessionScope::class) class DefaultSessionEnterpriseService : SessionEnterpriseService { override suspend fun init() = Unit + override suspend fun tweakMasUrl(url: String): String = url override suspend fun isElementCallAvailable(): Boolean = true } diff --git a/features/enterprise/test/build.gradle.kts b/features/enterprise/test/build.gradle.kts index 542e73717a..c37fc53de3 100644 --- a/features/enterprise/test/build.gradle.kts +++ b/features/enterprise/test/build.gradle.kts @@ -15,6 +15,7 @@ android { dependencies { api(projects.features.enterprise.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.compound) implementation(projects.libraries.matrix.api) implementation(projects.tests.testutils) diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 3c17a4de7c..805c75be6a 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -30,6 +30,7 @@ class FakeEnterpriseService( private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() }, + private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() }, ) : EnterpriseService { private val brandColorState = MutableStateFlow(initialBrandColor) private val semanticColorsState = MutableStateFlow(initialSemanticColors) @@ -38,6 +39,10 @@ class FakeEnterpriseService( isEnterpriseUserResult(sessionId) } + override suspend fun tweakMasUrl(url: String, homeserver: String): String = simulateLongTask { + tweakMasUrlResult(url, homeserver) + } + override fun defaultHomeserverList(): List { return defaultHomeserverListResult() } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt index 3914c60155..0bcad13033 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt @@ -14,10 +14,15 @@ import io.element.android.tests.testutils.simulateLongTask class FakeSessionEnterpriseService( private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, + private val tweakMasUrlResult: (String) -> String = { lambdaError() }, ) : SessionEnterpriseService { override suspend fun init() { } + override suspend fun tweakMasUrl(url: String): String = simulateLongTask { + tweakMasUrlResult(url) + } + override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { isElementCallAvailableResult() } diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt index eb085d19c5..1c565a390a 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesNode.kt @@ -74,7 +74,10 @@ class ForwardMessagesNode( return roomSelectEntryPoint.createNode( parentNode = this, buildContext = buildContext, - params = RoomSelectEntryPoint.Params(mode = RoomSelectMode.Forward), + params = RoomSelectEntryPoint.Params( + mode = RoomSelectMode.Forward, + maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS, + ), callback = callback, ) } diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt index f1e9bd8fc6..a73478479e 100644 --- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt +++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.forward.impl 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -21,34 +23,29 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressTag -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ForwardMessagesViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ForwardMessagesViewTest : RobolectricTest() { @Test - fun `cancel error emits the expected event`() { + fun `cancel error emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setForwardMessagesView( + setForwardMessagesView( aForwardMessagesState( forwardAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError) } @Test - fun `success invokes onForwardSuccess`() { + fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest { val data = listOf(A_ROOM_ID) val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam?>(data) { callback -> - rule.setForwardMessagesView( + setForwardMessagesView( aForwardMessagesState( forwardAction = AsyncAction.Success(data), eventSink = eventsRecorder @@ -59,7 +56,7 @@ class ForwardMessagesViewTest { } } -private fun AndroidComposeTestRule.setForwardMessagesView( +private fun AndroidComposeUiTest.setForwardMessagesView( state: ForwardMessagesState, onForwardSuccess: (List) -> Unit = EnsureNeverCalledWithParam(), ) { diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt index 0ca25c9455..1bfa10daf2 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt @@ -19,6 +19,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -90,7 +93,11 @@ fun ChooseSelfVerificationModeView( Text( modifier = Modifier .clickable(onClick = onLearnMore) - .padding(vertical = 4.dp, horizontal = 16.dp), + .padding(vertical = 4.dp, horizontal = 16.dp) + .semantics { + // Note: there is no Role.Link, so we use Role.Button for better accessibility support + role = Role.Button + }, text = stringResource(CommonStrings.action_learn_more), style = ElementTheme.typography.fontBodyLgMedium ) diff --git a/features/ftue/impl/src/main/res/values-ca/translations.xml b/features/ftue/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..1b645dd5eb --- /dev/null +++ b/features/ftue/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,15 @@ + + + "No pots confirmar-la?" + "Crea nova clau de recuperació" + "Verifica aquest dispositiu per configurar missatges segurs." + "Confirma la teva identitat" + "Utilitza un altre dispositiu" + "Utilitza clau de recuperació" + "Ara pots llegir o enviar missatges de manera segura, i qualsevol persona amb qui xategis també confiarà en aquest dispositiu." + "Dispositiu verificat" + "Utilitza un altre dispositiu" + "Esperant un altre dispositiu…" + "Pots canviar la configuració més tard." + "Permet les notificacions i no perdis cap missatge" + diff --git a/features/ftue/impl/src/main/res/values-de/translations.xml b/features/ftue/impl/src/main/res/values-de/translations.xml index 241f73516e..0e83b29e35 100644 --- a/features/ftue/impl/src/main/res/values-de/translations.xml +++ b/features/ftue/impl/src/main/res/values-de/translations.xml @@ -3,7 +3,7 @@ "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" "Wähle eine Verifizierungsmethode, um den sicheren Nachrichtenversand einzurichten." - "Bestätige deine Identität" + "Bestätige deine digitale Identität" "Ein anderes Gerät verwenden" "Wiederherstellungsschlüssel verwenden" "Du kannst jetzt verschlüsselte Nachrichten lesen und versenden. Dein Chatpartner vertraut nun diesem Gerät." diff --git a/features/ftue/impl/src/main/res/values-et/translations.xml b/features/ftue/impl/src/main/res/values-et/translations.xml index 3e961d6249..c013556568 100644 --- a/features/ftue/impl/src/main/res/values-et/translations.xml +++ b/features/ftue/impl/src/main/res/values-et/translations.xml @@ -2,7 +2,7 @@ "Kas kinnitamine pole võimalik?" "Loo uus taastevõti" - "Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade." + "Turvalise sõnumside seadistamiseks vali verifitseerimise viis." "Kinnita oma digitaalne identiteet" "Kasuta teist seadet" "Kasuta taastevõtit" diff --git a/features/ftue/impl/src/main/res/values-ja/translations.xml b/features/ftue/impl/src/main/res/values-ja/translations.xml index 68b69079ef..9a87a3dcfa 100644 --- a/features/ftue/impl/src/main/res/values-ja/translations.xml +++ b/features/ftue/impl/src/main/res/values-ja/translations.xml @@ -11,5 +11,5 @@ "他の端末を使用" "一方の端末を待機中…" "設定は後で変更することができます。" - "メッセージを見逃さないため通知を許可" + "メッセージを見逃さないために通知を許可しましょう" diff --git a/features/ftue/impl/src/main/res/values-pl/translations.xml b/features/ftue/impl/src/main/res/values-pl/translations.xml index 5d77c57994..45ca82ede6 100644 --- a/features/ftue/impl/src/main/res/values-pl/translations.xml +++ b/features/ftue/impl/src/main/res/values-pl/translations.xml @@ -2,8 +2,8 @@ "Nie możesz potwierdzić?" "Utwórz nowy klucz przywracania" - "Zweryfikuj to urządzenie, aby skonfigurować bezpieczne przesyłanie wiadomości." - "Potwierdź, że to Ty" + "Wybierz sposób weryfikacji, aby skonfigurować bezpieczne wiadomości." + "Potwierdź swoją tożsamość cyfrową" "Użyj innego urządzenia" "Użyj klucza przywracania" "Teraz możesz bezpiecznie czytać i wysyłać wiadomości, każdy z kim czatujesz również może ufać temu urządzeniu." diff --git a/features/ftue/impl/src/main/res/values-pt/translations.xml b/features/ftue/impl/src/main/res/values-pt/translations.xml index 5b6729f04e..34f39c7bdb 100644 --- a/features/ftue/impl/src/main/res/values-pt/translations.xml +++ b/features/ftue/impl/src/main/res/values-pt/translations.xml @@ -3,7 +3,7 @@ "Não é possível confirmar?" "Criar uma nova chave de recuperação" "Verifica este dispositivo para configurar o envio seguro de mensagens." - "Confirma que és tu" + "Confirma a tua identidade digital" "Utilizar outro dispositivo" "Utilizar chave de recuperação" "Agora podes ler ou enviar mensagens de forma segura, e qualquer pessoa com quem converses também pode confiar neste dispositivo." 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 34ac77ae3f..b7c0055b73 100644 --- a/features/ftue/impl/src/main/res/values-ru/translations.xml +++ b/features/ftue/impl/src/main/res/values-ru/translations.xml @@ -6,7 +6,7 @@ "Подтвердите личность" "Использовать другое устройство" "Использовать ключ восстановления" - "Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству." + "Теперь Вы можете безопасно читать и отправлять сообщения, и все, с кем Вы общаетесь в чате, также могут доверять этому устройству." "Устройство проверено" "Использовать другое устройство" "Ожидание другого устройства…" 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 c111aecaa9..f9171c9879 100644 --- a/features/ftue/impl/src/main/res/values-zh/translations.xml +++ b/features/ftue/impl/src/main/res/values-zh/translations.xml @@ -9,7 +9,7 @@ "现在你可以安全地读取或发送消息,并且与你聊天的任何人也可以信任此设备。" "设备已验证" "使用其它设备" - "正在等待其它设备……" + "正在等待其它设备…" "你可以稍后更改设置。" "允许通知,绝不错过任何消息" diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt index 521bf91b37..66e2eb38fe 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt @@ -6,77 +6,74 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.ftue.impl.sessionverification.choosemode 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.ftue.impl.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class ChooseSessionVerificationModeViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class ChooseSessionVerificationModeViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test - fun `clicking on learn more invokes the expected callback`() { + fun `clicking on learn more invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(), onLearnMoreClick = callback, ) - rule.clickOn(CommonStrings.action_learn_more) + clickOn(CommonStrings.action_learn_more) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on use another device calls the callback`() { + fun `clicking on use another device calls the callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))), onUseAnotherDevice = callback, ) - rule.clickOn(R.string.screen_identity_use_another_device) + clickOn(R.string.screen_identity_use_another_device) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on enter recovery key calls the callback`() { + fun `clicking on enter recovery key calls the callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseRecoveryKey = true))), onEnterRecoveryKey = callback, ) - rule.clickOn(R.string.screen_identity_confirmation_use_recovery_key) + clickOn(R.string.screen_identity_confirmation_use_recovery_key) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on cannot confirm calls the reset keys callback`() { + fun `clicking on cannot confirm calls the reset keys callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setChooseSelfVerificationModeView( + setChooseSelfVerificationModeView( aChooseSelfVerificationModeState(), onResetKey = callback, ) - rule.clickOn(R.string.screen_identity_confirmation_cannot_confirm) + clickOn(R.string.screen_identity_confirmation_cannot_confirm) } } - private fun AndroidComposeTestRule.setChooseSelfVerificationModeView( + private fun AndroidComposeUiTest.setChooseSelfVerificationModeView( state: ChooseSelfVerificationModeState, onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onUseAnotherDevice: () -> Unit = EnsureNeverCalled(), diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index b36ee6aed2..0635da39a5 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.libraries.permissions.noop) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.features.announcement.api) implementation(projects.features.invite.api) implementation(projects.features.networkmonitor.api) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt index 81e7969080..00f285e3f4 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.bumble.appyx.core.lifecycle.subscribe @@ -171,6 +172,7 @@ class HomeFlowNode( if (loadingJoinedRoomJob.value.isLoading()) { DelayedVisibility(duration = 400.milliseconds) { ProgressDialog( + properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true), onDismissRequest = { loadingJoinedRoomJob.value.dataOrNull()?.cancel() loadingJoinedRoomJob.value = AsyncData.Uninitialized diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index ff0fc00496..a563520e09 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -10,6 +10,7 @@ package io.element.android.features.home.impl.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -57,6 +58,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGradient import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem @@ -65,8 +67,8 @@ 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.core.SessionId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.testtags.TestTags @@ -159,7 +161,7 @@ fun HomeTopBar( } @Composable -private fun RoomListMenuItems( +private fun RowScope.RoomListMenuItems( onToggleSearch: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, canReportBug: Boolean, @@ -237,6 +239,7 @@ private fun SpaceFilterButton( else -> Unit } } + val isSelected = spaceFiltersState is SpaceFiltersState.Selected IconButton( onClick = ::onClick, @@ -320,7 +323,15 @@ private fun AccountIcon( Avatar( avatarData = avatarData, avatarType = AvatarType.User, - contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null, + contentDescription = if (isCurrentAccount) { + if (showAvatarIndicator) { + stringResource(CommonStrings.a11y_settings_with_required_action) + } else { + stringResource(CommonStrings.common_settings) + } + } else { + null + }, ) if (showAvatarIndicator) { RedIndicatorAtom( @@ -337,7 +348,7 @@ private fun AccountIcon( internal fun HomeTopBarPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), @@ -358,7 +369,7 @@ internal fun HomeTopBarPreview() = ElementPreview { internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), @@ -379,7 +390,7 @@ internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { internal fun HomeTopBarSpacesPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Spaces, - currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), showAvatarIndicator = false, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), @@ -400,7 +411,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview { internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + currentUserAndNeighbors = persistentListOf(aMatrixUser(id = "@id:domain", displayName = USER_NAME_ALICE)), showAvatarIndicator = true, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index a03399baf7..266b6f5a59 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -245,19 +245,25 @@ private fun RoomsViewList( ) } } - SecurityBannerState.None -> if (state.fullScreenIntentPermissionsState.shouldDisplayBanner) { - item { - FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState) + // Banner precedence (top-to-bottom): full-screen-intent > battery-optimization > + // new-notification-sound > sound-unavailable. At most one renders at a time. + SecurityBannerState.None -> when { + state.fullScreenIntentPermissionsState.shouldDisplayBanner -> { + item { + FullScreenIntentPermissionBanner(state = state.fullScreenIntentPermissionsState) + } } - } else if (state.batteryOptimizationState.shouldDisplayBanner) { - item { - BatteryOptimizationBanner(state = state.batteryOptimizationState) + state.batteryOptimizationState.shouldDisplayBanner -> { + item { + BatteryOptimizationBanner(state = state.batteryOptimizationState) + } } - } else if (state.showNewNotificationSoundBanner) { - item { - NewNotificationSoundBanner( - onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) }, - ) + state.showNewNotificationSoundBanner -> { + item { + NewNotificationSoundBanner( + onDismissClick = { eventSink(RoomListEvent.DismissNewNotificationSoundBanner) }, + ) + } } } } @@ -273,6 +279,7 @@ private fun RoomsViewList( hideInviteAvatars = hideInvitesAvatars, isInviteSeen = room.displayType == RoomSummaryDisplayType.INVITE && state.seenRoomInvites.contains(room.roomId), + showUnreadCount = state.showUnreadCount, onClick = onRoomClick, eventSink = eventSink, ) 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 f541417104..ba9a204a64 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 @@ -29,6 +29,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -75,8 +76,9 @@ internal fun RoomSummaryRow( hideInviteAvatars: Boolean, isInviteSeen: Boolean, onClick: (RoomListRoomSummary) -> Unit, - eventSink: (RoomListEvent) -> Unit, modifier: Modifier = Modifier, + showUnreadCount: Boolean = false, + eventSink: (RoomListEvent) -> Unit, ) { Box(modifier = modifier) { when (room.displayType) { @@ -126,7 +128,7 @@ internal fun RoomSummaryRow( timestamp = room.timestamp, isHighlighted = room.isHighlighted ) - MessagePreviewAndIndicatorRow(room = room) + MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount) } } RoomSummaryDisplayType.KNOCKED -> { @@ -222,20 +224,17 @@ private fun NameAndTimestampRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = spacedBy(16.dp) ) { - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - ) { - // Name - Text( - style = ElementTheme.typography.fontBodyLgMedium, - text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name), - fontStyle = FontStyle.Italic.takeIf { name == null }, - color = ElementTheme.colors.roomListRoomName, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + Text( + modifier = Modifier + .weight(1f) + .clipToBounds(), + style = ElementTheme.typography.fontBodyLgMedium, + text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name), + fontStyle = FontStyle.Italic.takeIf { name == null }, + color = ElementTheme.colors.roomListRoomName, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) // Timestamp Text( text = timestamp ?: "", @@ -262,12 +261,12 @@ private fun InviteSubtitle( } if (subtitle != null) { Text( + modifier = modifier.clipToBounds(), text = subtitle, maxLines = 1, overflow = TextOverflow.Ellipsis, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.roomListRoomMessage, - modifier = modifier, ) } } @@ -275,6 +274,7 @@ private fun InviteSubtitle( @Composable private fun MessagePreviewAndIndicatorRow( room: RoomListRoomSummary, + showUnreadCount: Boolean, modifier: Modifier = Modifier, ) { Row( @@ -326,7 +326,9 @@ private fun MessagePreviewAndIndicatorRow( val messagePreview = room.latestEvent.content() val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.orEmpty().toString()) Text( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .clipToBounds(), text = annotatedMessagePreview, color = ElementTheme.colors.roomListRoomMessage, style = ElementTheme.typography.fontBodyMdRegular, @@ -360,8 +362,18 @@ private fun MessagePreviewAndIndicatorRow( } if (room.hasNewContent) { val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages) + val count = if (showUnreadCount) { + if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) { + room.numberOfUnreadMessages + } else { + room.numberOfUnreadNotifications + } + } else { + null + } UnreadIndicatorAtom( color = tint, + count = count, contentDescription = contentDescription, ) } @@ -381,7 +393,9 @@ private fun InviteNameAndIndicatorRow( verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .clipToBounds(), style = ElementTheme.typography.fontBodyLgMedium, text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name), fontStyle = FontStyle.Italic.takeIf { name == null }, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt index 3ff4339bb2..56b2c1ade4 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSource.kt @@ -83,8 +83,8 @@ class RoomListDataSource( val loadingState = roomList.loadingState - fun launchIn(coroutineScope: CoroutineScope) { - roomList + fun launchIn(coroutineScope: CoroutineScope): Job { + return roomList .summaries .onEach { roomSummaries -> replaceWith(roomSummaries) @@ -212,6 +212,7 @@ class RoomListDataSource( private suspend fun rebuildAllRoomSummaries() { lock.withLock { roomList.summaries.replayCache.firstOrNull()?.let { roomSummaries -> + diffCacheUpdater.updateWith(roomSummaries) buildAndEmitAllRooms(roomSummaries, useCache = false) } } 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 26054d7e56..e34f2845da 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 @@ -19,7 +19,6 @@ 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 import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.getAvatarData 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 eefb2d6484..09e6c2e6c9 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 @@ -11,6 +11,10 @@ package io.element.android.features.home.impl.model import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.LAST_MESSAGE +import io.element.android.libraries.designsystem.preview.ROOM_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB 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 @@ -85,16 +89,16 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider = aRoomListRoomSummaryList(), fullScreenIntentPermissionsState: FullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), batteryOptimizationState: BatteryOptimizationState = aBatteryOptimizationState(), @@ -43,6 +44,7 @@ internal fun aRoomsContentState( ) = RoomListContentState.Rooms( securityBannerState = securityBannerState, showNewNotificationSoundBanner = showNewNotificationSoundBanner, + showUnreadCount = showUnreadCount, fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, batteryOptimizationState = batteryOptimizationState, summaries = summaries, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt index 66982961fd..e760379d7b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt @@ -69,10 +69,6 @@ fun RoomListContextMenu( onFavoriteChange = { isFavorite -> eventSink(RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, isFavorite)) }, - onClearCacheRoomClick = { - eventSink(RoomListEvent.HideContextMenu) - eventSink(RoomListEvent.ClearCacheOfRoom(contextMenu.roomId)) - }, onReportRoomClick = { eventSink(RoomListEvent.HideContextMenu) onReportRoomClick(contextMenu.roomId) @@ -90,7 +86,6 @@ private fun RoomListModalBottomSheetContent( onFavoriteChange: (isFavorite: Boolean) -> Unit, onRoomMarkReadClick: () -> Unit, onRoomMarkUnreadClick: () -> Unit, - onClearCacheRoomClick: () -> Unit, onReportRoomClick: () -> Unit, ) { Column( @@ -119,7 +114,6 @@ private fun RoomListModalBottomSheetContent( leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector(CompoundIcons.MarkAsRead()) ), - style = ListItemStyle.Primary, ) } else { ListItem( @@ -133,7 +127,6 @@ private fun RoomListModalBottomSheetContent( leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector(CompoundIcons.MarkAsUnread()) ), - style = ListItemStyle.Primary, ) } val (textResId, icon) = if (contextMenu.isFavorite) { @@ -159,7 +152,6 @@ private fun RoomListModalBottomSheetContent( onClick = { onFavoriteChange(!contextMenu.isFavorite) }, - style = ListItemStyle.Primary, ) ListItem( headlineContent = { @@ -174,7 +166,6 @@ private fun RoomListModalBottomSheetContent( CompoundIcons.Settings(), ) ), - style = ListItemStyle.Primary, ) if (canReportRoom) { ListItem( @@ -202,18 +193,6 @@ private fun RoomListModalBottomSheetContent( ), style = ListItemStyle.Destructive, ) - if (contextMenu.displayClearRoomCacheAction) { - ListItem( - headlineContent = { - Text(text = "Clear cache for this room") - }, - modifier = Modifier.clickable { onClearCacheRoomClick() }, - leadingContent = ListItemContent.Icon( - iconSource = IconSource.Vector(CompoundIcons.Delete()) - ), - style = ListItemStyle.Primary, - ) - } } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt index 3e82ff9809..49d033682f 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListEvent.kt @@ -30,5 +30,4 @@ sealed interface RoomListEvent { data class MarkAsRead(val roomId: RoomId) : ContextMenuEvent data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvent data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvent - data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvent } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 2010555cd7..128e663f61 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -28,7 +28,6 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.datasource.RoomListDataSource -import io.element.android.features.home.impl.filters.RoomListFilter.Rooms import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.filters.into import io.element.android.features.home.impl.search.RoomListSearchEvent @@ -44,6 +43,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.architecture.AsyncData 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.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -52,7 +53,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.push.api.battery.BatteryOptimizationState import io.element.android.libraries.push.api.notifications.NotificationCleaner @@ -85,11 +85,11 @@ class RoomListPresenter( private val fullScreenIntentPermissionsPresenter: Presenter, private val batteryOptimizationPresenter: Presenter, private val notificationCleaner: NotificationCleaner, - private val appPreferencesStore: AppPreferencesStore, private val seenInvitesStore: SeenInvitesStore, private val announcementService: AnnouncementService, private val coldStartWatcher: AnalyticsColdStartWatcher, private val spaceFiltersPresenter: Presenter, + private val featureFlagService: FeatureFlagService, ) : Presenter { private val encryptionService = client.encryptionService @@ -154,7 +154,6 @@ class RoomListPresenter( } is RoomListEvent.ShowDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Shown(event.roomSummary) RoomListEvent.HideDeclineInviteMenu -> declineInviteMenu.value = RoomListState.DeclineInviteMenu.Hidden - is RoomListEvent.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId) } } @@ -165,13 +164,17 @@ class RoomListPresenter( roomListDataSource.updateFilter(allFilters) } + val canReportRoom by produceState(false) { value = client.canReportRoom() } + val showUnreadCount by produceState(false) { + value = featureFlagService.isFeatureEnabled(FeatureFlags.UnreadIndicatorCount) + } + val contentState = roomListContentState( securityBannerDismissed, showNewNotificationSoundBanner, + showUnreadCount, ) - val canReportRoom by produceState(false) { value = client.canReportRoom() } - return RoomListState( contextMenu = contextMenu.value, declineInviteMenu = declineInviteMenu.value, @@ -226,6 +229,7 @@ class RoomListPresenter( private fun roomListContentState( securityBannerDismissed: Boolean, showNewNotificationSoundBanner: Boolean, + showUnreadCount: Boolean, ): RoomListContentState { val roomSummaries by produceState(initialValue = AsyncData.Loading()) { roomListDataSource.roomSummariesFlow.collect { value = AsyncData.Success(it) } @@ -254,6 +258,7 @@ class RoomListPresenter( RoomListContentState.Rooms( securityBannerState = securityBannerState, showNewNotificationSoundBanner = showNewNotificationSoundBanner, + showUnreadCount = showUnreadCount, fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), batteryOptimizationState = batteryOptimizationPresenter.present(), summaries = roomSummaries.dataOrNull().orEmpty().toImmutableList(), @@ -271,7 +276,6 @@ class RoomListPresenter( isDm = event.roomSummary.isDm, isFavorite = event.roomSummary.isFavorite, hasNewContent = event.roomSummary.hasNewContent, - displayClearRoomCacheAction = appPreferencesStore.isDeveloperModeEnabledFlow().first(), ) contextMenuState.value = initialState @@ -327,10 +331,4 @@ class RoomListPresenter( } } } - - private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch { - client.getRoom(roomId)?.use { room -> - room.clearEventCacheStorage() - } - } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index e0f4943621..e184a5a57f 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -44,7 +44,6 @@ data class RoomListState( val isDm: Boolean, val isFavorite: Boolean, val hasNewContent: Boolean, - val displayClearRoomCacheAction: Boolean, ) : ContextMenu } @@ -72,6 +71,7 @@ sealed interface RoomListContentState { val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, val batteryOptimizationState: BatteryOptimizationState, val showNewNotificationSoundBanner: Boolean, + val showUnreadCount: Boolean, val summaries: ImmutableList, val seenRoomInvites: ImmutableSet, ) : RoomListContentState diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt index a1624e4917..eca1b63d7d 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt @@ -31,5 +31,4 @@ internal fun aContextMenuShown( isDm = isDm, hasNewContent = hasNewContent, isFavorite = isFavorite, - displayClearRoomCacheAction = false, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt index 1f0d6ff7d9..8a270f1b85 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -48,6 +48,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -93,7 +94,7 @@ fun SpaceFiltersView( SpaceFiltersBottomSheetContent( filters = state.visibleFilters, searchQuery = state.searchQuery, - onFilterSelected = { filter -> + onSelectFilter = { filter -> state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) } ) @@ -105,9 +106,9 @@ fun SpaceFiltersView( @Composable private fun SpaceFiltersBottomSheetContent( - filters: List, + filters: ImmutableList, searchQuery: TextFieldState, - onFilterSelected: (SpaceServiceFilter) -> Unit, + onSelectFilter: (SpaceServiceFilter) -> Unit, modifier: Modifier = Modifier ) { Column( @@ -133,7 +134,7 @@ private fun SpaceFiltersBottomSheetContent( items(filters) { filter -> SpaceFilterItem( filter = filter, - onClick = { onFilterSelected(filter) } + onClick = { onSelectFilter(filter) } ) } } diff --git a/features/home/impl/src/main/res/values-ca/translations.xml b/features/home/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..e37db27f23 --- /dev/null +++ b/features/home/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,49 @@ + + + "Desactiva l\'optimització de bateria d\'aquesta aplicació per assegurar-te de rebre totes les notificacions." + "No arriben les notificacions?" + "Recupera la teva identitat criptogràfica i l\'historial de missatges amb una clau de recuperació si has perdut l\'accés a tots els teus dispositius existents." + "Configura la recuperació" + "Configura la recuperació per protegir el teu compte" + "Confirma la clau de recuperació per mantenir l\'accés a l\'emmagatzematge de claus i a l\'historial de missatges." + "Introdueix clau de recuperació" + "Has oblidat la clau de recuperació?" + "L\'emmagatzematge de claus no està sincronitzat" + "Per assegurar que mai et perdis una trucada important, canvia la configuració per permetre les notificacions en pantalla completa quan el telèfon està bloquejat." + "Millora l\'experiència de les trucades" + "Xats" + "Segur que vols rebutjar la invitació per unir-te a %1$s?" + "Rebutja invitació" + "Segur que vols rebutjar el xat privat amb %1$s?" + "Rebutja xat" + "Sense invitacions" + "%1$s (%2$s) t\'ha convidat" + "Aquest procés només s\'ha de fer una vegada, gràcies per esperar." + "Configurant compte." + "Crea un nou xat o sala" + "Comença enviant un missatge a algú." + "Encara no hi ha xats." + "Preferits" + "Pots afegir un xat a preferits a la configuració del xat. +De moment, pots desseleccionar els filtres per veure tots els xats." + "Encara no tens cap xat preferit" + "Invitacions" + "No tens cap invitació pendent." + "Prioritat baixa" + "Pots desseleccionar els filtres per veure els altres xats" + "Cap xats per a aquesta selecció" + "Persones" + "Encara no tens cap xat directe" + "Sales" + "Encara no pertanys a cap sala" + "No llegits" + "Enhorabona! +No tens missatges sense llegir!" + "Sol·licitud d\'unió enviada" + "Xats" + "Marca com a llegit" + "Marca com a no llegit" + "La sala ha estat actualitzada" + "Sembla que estàs utilitzant un dispositiu nou. Verifica\'l amb un altre dispositiu per accedir als teus missatges xifrats." + "Verifica que ets tu" + diff --git a/features/home/impl/src/main/res/values-et/translations.xml b/features/home/impl/src/main/res/values-et/translations.xml index c1f61bfa29..4ba1695d7e 100644 --- a/features/home/impl/src/main/res/values-et/translations.xml +++ b/features/home/impl/src/main/res/values-et/translations.xml @@ -5,9 +5,9 @@ "Sa ei näe kõiki teavitusi?" "Sinu nutiseadme teavituste heli on uuenenud - see on nüüd selgem, kiirem ja vähem häiriv." "Oleme sinu helisid värskendanud" - "Loo uus taastevõti, mida saad kasutada oma krüptitud sõnumite ajaloo taastamisel olukorras, kus kaotad ligipääsu oma seadmetele." - "Seadista andmete taastamine" - "Seadista taastamine" + "Sinu vestlused on automaatselt varundatud kasutades läbivat krüptimist. Kui peaksid kaotama ligipääsu kõikidele oma seadmetele, siis selle varukoopia taastamiseks ja oma digitaalse identiteedi säilitamiseks, on vaja taastevõtit." + "Seadista taastevõti" + "Varunda oma vestlused" "Säilitamaks ligipääsu vestluste ja krüptovõtmete varukoopiale, palun sisesta kinnituseks oma taastevõti." "Sisesta oma taastevõti" "Kas unustasid oma taastevõtme?" diff --git a/features/home/impl/src/main/res/values-fa/translations.xml b/features/home/impl/src/main/res/values-fa/translations.xml index aec50309d8..b0c6a30bc4 100644 --- a/features/home/impl/src/main/res/values-fa/translations.xml +++ b/features/home/impl/src/main/res/values-fa/translations.xml @@ -4,7 +4,7 @@ "از کار انداختن بهینه سازی" "آگاهی‌ها نمی‌رسند؟" "بازگردانی تاریخچهٔ پیام‌ها و هویت رمزنگاشته‌تان با کلید بازیابی در صورت از دست دادن همهٔ افزاره‌های موجودتان." - "برپایی بازیابی" + "دریافت کلید بازیابی" "برپایی بازیابی" "کلید بازیابی خود را تأیید کنید تا دسترسی به حافظه کلیدها و تاریخچه پیام‌هایتان حفظ شود ." "ورود کلید بازیابیتان" @@ -25,6 +25,7 @@ "آغاز با پیام دادن به کسی." "هنوز گپی وجود ندارد." "علاقه‌مندی‌ها" + "می‌توانید در تنظیمات چت، یک چت را به موارد دلخواه خود اضافه کنید. فعلاً می‌توانید فیلترها را غیرفعال کنید تا چت‌های دیگر خود را ببینید." "هنوز هیچ گپ مورد علاقه‌ای ندارید" "دعوت‌ها" "هیچ دعوت منتظری ندارید." diff --git a/features/home/impl/src/main/res/values-ja/translations.xml b/features/home/impl/src/main/res/values-ja/translations.xml index c1c1ade1ff..855bb14533 100644 --- a/features/home/impl/src/main/res/values-ja/translations.xml +++ b/features/home/impl/src/main/res/values-ja/translations.xml @@ -1,7 +1,7 @@ "すべての通知を確実に受信するために、このアプリのバッテリー最適化を無効にしてください。" - "最適化を無効にする" + "無効にする" "通知が届いていませんか?" "通知音が更新され、より明確で速く、そして邪魔にならなくなりました。" "サウンドを刷新しました" @@ -38,7 +38,7 @@ "低い優先度のチャットはまだありません" "フィルターを解除して他のチャットを表示できます" "この選択中にチャットがありません" - "人々" + "ユーザー" "まだダイレクトメッセージは届いていません" "ルーム" "まだルームに参加していません" diff --git a/features/home/impl/src/main/res/values-pl/translations.xml b/features/home/impl/src/main/res/values-pl/translations.xml index 73e0b5e52e..0c1b07f27b 100644 --- a/features/home/impl/src/main/res/values-pl/translations.xml +++ b/features/home/impl/src/main/res/values-pl/translations.xml @@ -5,9 +5,9 @@ "Powiadomienia nie dochodzą?" "Sygnał powiadomień został zaktualizowany — jest wyraźniejszy, szybszy i mniej uciążliwy." "Odświeżyliśmy Twoje dźwięki" - "Wygeneruj nowy klucz przywracania, którego można użyć do przywrócenia historii wiadomości szyfrowanych w przypadku utraty dostępu do swoich urządzeń." - "Skonfiguruj przywracanie" - "Skonfiguruj przywracanie" + "Twoje czaty są automatycznie archiwizowane za pomocą szyfrowania end-to-end. Aby przywrócić tę kopię zapasową i swoją tożsamość cyfrową, wymagany będzie klucz przywracania." + "Uzyskaj klucz przywracania" + "Utwórz kopię zapasową swoich czatów" "Potwierdź klucz przywracania, aby zachować dostęp do magazynu kluczy i historii wiadomości." "Wprowadź klucz przywracania" "Zapomniałeś klucza przywracania?" @@ -50,6 +50,7 @@ Nie masz żadnych nieprzeczytanych wiadomości!" "Oznacz jako przeczytane" "Oznacz jako nieprzeczytane" "Ten pokój został ulepszony" + "Twoje przestrzenie" "Wygląda na to, że używasz nowego urządzenia. Zweryfikuj się innym urządzeniem, aby uzyskać dostęp do zaszyfrowanych wiadomości." "Potwierdź, że to Ty" diff --git a/features/home/impl/src/main/res/values-pt/translations.xml b/features/home/impl/src/main/res/values-pt/translations.xml index 9870014904..6c6a72930e 100644 --- a/features/home/impl/src/main/res/values-pt/translations.xml +++ b/features/home/impl/src/main/res/values-pt/translations.xml @@ -6,7 +6,7 @@ "O toque de notificação foi atualizado — mais claro, mais rápido e menos perturbador." "Atualizámos os seus sons" "Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação se tiveres perdido todos os teus dispositivos existentes." - "Configurar recuperação" + "Chave de recuperação" "Configurar a recuperação" "Confirma a tua chave de recuperação para manteres o acesso ao teu armazenamento de chaves e ao histórico de mensagens." "Introduz a tua chave de recuperação" diff --git a/features/home/impl/src/main/res/values-ro/translations.xml b/features/home/impl/src/main/res/values-ro/translations.xml index be1933e160..14cf100c22 100644 --- a/features/home/impl/src/main/res/values-ro/translations.xml +++ b/features/home/impl/src/main/res/values-ro/translations.xml @@ -50,6 +50,7 @@ Nu aveți mesaje necitite!" "Marcați ca citită" "Marcați ca necitită" "Această cameră a fost modernizată." + "Spațiile dumneavoastră" "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea cu un alt dispozitiv pentru a accesa mesajele dumneavoastră criptate." "Verificați că sunteți dumneavoastră" 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 233e8749b4..84a604e184 100644 --- a/features/home/impl/src/main/res/values-ru/translations.xml +++ b/features/home/impl/src/main/res/values-ru/translations.xml @@ -21,7 +21,7 @@ "Вы уверены, что хотите отказаться от личного общения с %1$s?" "Отклонить чат" "Нет приглашений" - "%1$s (%2$s) пригласил(а) вас" + "%1$s (%2$s) пригласил(а) Вас" "Это единоразовая процедура, спасибо, что подождали." "Настройка Вашего аккаунта." "Создать новую беседу или комнату" @@ -31,20 +31,20 @@ "Избранные" "Добавить чат в избранное можно в настройках чата. На данный момент вы можете убрать фильтры, чтобы увидеть другие ваши чаты." - "У вас пока нет избранных чатов" + "У Вас пока нет избранных чатов" "Приглашения" - "У вас нет приглашений." + "У Вас нет приглашений." "Низкий приоритет" - "У вас пока нет чатов с низким приоритетом." + "У Вас пока нет чатов с низким приоритетом." "Вы можете убрать фильтры, чтобы увидеть другие ваши чаты." - "У вас нет чатов, соответствующих фильтрам" + "У Вас нет чатов, соответствующих фильтрам" "Пользователи" - "У вас пока нет личных сообщений" + "У Вас пока нет личных сообщений" "Комнаты" "Вас пока нет ни в одной комнате" "Новые" "Поздравляем! -У вас нет непрочитанных сообщений!" +У Вас нет непрочитанных сообщений!" "Запрос на присоединение отправлен" "Чаты" "Пометить как прочитанное" diff --git a/features/home/impl/src/main/res/values-sk/translations.xml b/features/home/impl/src/main/res/values-sk/translations.xml index 12e6284468..aab1f6dc1d 100644 --- a/features/home/impl/src/main/res/values-sk/translations.xml +++ b/features/home/impl/src/main/res/values-sk/translations.xml @@ -6,7 +6,7 @@ "Vaše oznámenia boli aktualizované – sú prehľadnejšie, rýchlejšie a menej rušivé." "Obnovili sme vaše zvuky" "Vytvorte nový kľúč na obnovenie, ktorý môžete použiť na obnovenie vašej histórie šifrovaných správ v prípade straty prístupu k vašim zariadeniam." - "Nastaviť obnovenie" + "Získať kľúč na obnovenie" "Nastaviť obnovenie" "Potvrďte svoj kľúč na obnovenie, aby ste zachovali prístup k úložisku kľúčov a histórii správ." "Zadajte kľúč na obnovenie" 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 4bc15b08cf..575adc6bb9 100644 --- a/features/home/impl/src/main/res/values-zh/translations.xml +++ b/features/home/impl/src/main/res/values-zh/translations.xml @@ -4,7 +4,7 @@ "禁用优化" "通知未送达?" "通知提示音已升级:更清晰、更快速、干扰更少。" - "我们已更新你的声音" + "我们已更新提示音" "你的聊天已被端到端加密自动备份。如果你无法访问所有设备,则需要使用恢复密钥并保留数字身份。" "获取恢复密钥" "备份聊天" @@ -21,14 +21,14 @@ "你确定要拒绝与 %1$s 私聊?" "拒绝聊天" "没有邀请" - "%1$s (%2$s)邀请了你" + "%1$s(%2$s)邀请了你" "此为一次性流程,感谢等待。" "设置账户。" "创建新的对话或房间" "清除筛选条件" "通过向某人发送消息来开始。" "暂无聊天。" - "收藏夹" + "收藏" "可以在聊天设置里将聊天添加到收藏夹。 现在可以取消选择筛选器以查看其它对话。" "你尚未收藏任何聊天" @@ -38,7 +38,7 @@ "你暂无任何低优先级聊天" "你可以取消选择筛选器以查看其它对话" "你暂无适用于此选项的聊天" - "用户" + "人员" "你暂无任何私聊" "房间" "你尚未进入任何房间" diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt index 582de66414..8eee8e1ee2 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt @@ -8,7 +8,6 @@ package io.element.android.features.home.impl -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat import io.element.android.features.home.api.HomeEntryPoint @@ -18,12 +17,11 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultHomeEntryPointTest { +class DefaultHomeEntryPointTest : RobolectricTest() { @Test fun `test node builder`() = runTest { val entryPoint = DefaultHomeEntryPoint() diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt index 1ce5061356..6df5e05df5 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/datasource/RoomListDataSourceTest.kt @@ -14,6 +14,9 @@ import io.element.android.features.home.impl.FakeDateTimeObserver import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID_2 +import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList @@ -100,11 +103,169 @@ class RoomListDataSourceTest { } } + /** + * Tracking issue #4182: rooms duplicated in the room list around midnight. + * + * If the SDK ever leaks a list containing the same roomId twice (the suspected cause of #4182), + * the UI mapper's `distinctBy` safety net in [RoomListDataSource.buildAndEmitAllRooms] must + * remove the duplicate AND `analyticsService.trackError` must fire so the team can root-cause + * it via Sentry. + */ + @Test + fun `when SDK summaries source contains duplicate roomIds, UI layer dedupes and reports trackError`() = runTest { + val analyticsService = FakeAnalyticsService() + val duplicatedSummaries = listOf( + aRoomSummary(roomId = A_ROOM_ID), + aRoomSummary(roomId = A_ROOM_ID), + aRoomSummary(roomId = A_ROOM_ID_2), + ) + val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(duplicatedSummaries)) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ).apply { + postState(RoomListService.State.Running) + } + val roomListDataSource = createRoomListDataSource( + roomListService = roomListService, + analyticsService = analyticsService, + ) + + roomListDataSource.roomSummariesFlow.test { + roomListDataSource.launchIn(backgroundScope) + val list = awaitItem() + assertThat(list.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder() + assertThat(analyticsService.trackedErrors).hasSize(1) + } + } + + /** + * Tracking issue #4182. + * + * Targeted scenario: a `DateChanged` tick fires after an initial SDK emit, then a follow-up + * SDK emit lands (mimicking "midnight, then a new message arrives"). Even though the diffCache + * is bypassed during the rebuild (`useCache = false`), the final state must contain each + * roomId exactly once and trackError must not fire on a happy path. + */ + @Test + fun `interleaved date change and SDK update with overlapping content does not produce duplicates`() = runTest { + val analyticsService = FakeAnalyticsService() + val summariesFlow = MutableStateFlow( + listOf( + aRoomSummary(roomId = A_ROOM_ID), + aRoomSummary(roomId = A_ROOM_ID_2), + ) + ) + val roomList = FakeDynamicRoomList(summaries = summariesFlow) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ).apply { + postState(RoomListService.State.Running) + } + val dateTimeObserver = FakeDateTimeObserver() + val roomListDataSource = createRoomListDataSource( + roomListService = roomListService, + dateTimeObserver = dateTimeObserver, + analyticsService = analyticsService, + ) + + roomListDataSource.roomSummariesFlow.test { + roomListDataSource.launchIn(backgroundScope) + val initial = awaitItem() + assertThat(initial.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder() + + // Midnight ticks while the cache holds [A_ROOM_ID, A_ROOM_ID_2] + dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now())) + val afterMidnight = awaitItem() + assertThat(afterMidnight.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder() + + // A new message bumps A_ROOM_ID — different unread count makes the StateFlow see this + // as a new value + summariesFlow.value = listOf( + aRoomSummary(roomId = A_ROOM_ID, numUnreadMessages = 1), + aRoomSummary(roomId = A_ROOM_ID_2), + ) + val afterMessage = awaitItem() + assertThat(afterMessage.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder() + assertThat(afterMessage.map { it.roomId }.toSet()).hasSize(afterMessage.size) + + // Second midnight rebuild after the new message + dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now())) + val afterSecondMidnight = awaitItem() + assertThat(afterSecondMidnight.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder() + assertThat(afterSecondMidnight.map { it.roomId }.toSet()).hasSize(afterSecondMidnight.size) + + assertThat(analyticsService.trackedErrors).isEmpty() + } + } + + @Test + fun `regression test for race with DateTimeObserver and new items`() = runTest { + val roomList = FakeDynamicRoomList(summaries = MutableStateFlow(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))) + val roomListService = FakeRoomListService( + createRoomListLambda = { roomList } + ).apply { + postState(RoomListService.State.Running) + } + val dateTimeObserver = FakeDateTimeObserver() + var dateFormatterResult = "Today" + val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult }) + val roomListDataSource = createRoomListDataSource( + roomListService = roomListService, + roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( + dateFormatter = dateFormatter, + ), + dateTimeObserver = dateTimeObserver, + ) + roomListDataSource.roomSummariesFlow.test { + // Observe room list items changes + val job = roomListDataSource.launchIn(backgroundScope) + // Get the initial room list + val initialRoomList = awaitItem() + assertThat(initialRoomList).hasSize(2) + assertThat(initialRoomList[0].roomId).isEqualTo(A_ROOM_ID) + assertThat(initialRoomList[0].timestamp).isEqualTo(dateFormatterResult) + assertThat(initialRoomList[1].roomId).isEqualTo(A_ROOM_ID_2) + assertThat(initialRoomList[1].timestamp).isEqualTo(dateFormatterResult) + + // Stop processing room list updates so we can force a race condition with the date time observer updates + job.cancel() + + // Trigger a date change and a new item at the same time + dateFormatterResult = "Yesterday" + roomList.summaries.tryEmit(listOf(aRoomSummary(roomId = A_ROOM_ID), aRoomSummary(roomId = A_ROOM_ID_3), aRoomSummary(roomId = A_ROOM_ID_2))) + dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now())) + + // The race condition would have caused the cache indices to be corrupted and only 2 items would be emitted + val rebuiltRoomList = awaitItem() + assertThat(rebuiltRoomList).hasSize(3) + assertThat(rebuiltRoomList[0].roomId).isEqualTo(A_ROOM_ID) + assertThat(rebuiltRoomList[0].timestamp).isEqualTo(dateFormatterResult) + assertThat(rebuiltRoomList[1].roomId).isEqualTo(A_ROOM_ID_3) + assertThat(rebuiltRoomList[1].timestamp).isEqualTo(dateFormatterResult) + assertThat(rebuiltRoomList[2].roomId).isEqualTo(A_ROOM_ID_2) + assertThat(rebuiltRoomList[2].timestamp).isEqualTo(dateFormatterResult) + + // Restart processing room list updates + roomListDataSource.launchIn(backgroundScope) + + // Check there is a new list and it's not the same as the previous one + val newRoomList = awaitItem() + assertThat(newRoomList).hasSize(3) + assertThat(newRoomList[0].roomId).isEqualTo(A_ROOM_ID) + assertThat(newRoomList[0].timestamp).isEqualTo(dateFormatterResult) + assertThat(newRoomList[1].roomId).isEqualTo(A_ROOM_ID_3) + assertThat(newRoomList[1].timestamp).isEqualTo(dateFormatterResult) + assertThat(newRoomList[2].roomId).isEqualTo(A_ROOM_ID_2) + assertThat(newRoomList[2].timestamp).isEqualTo(dateFormatterResult) + } + } + private fun TestScope.createRoomListDataSource( roomListService: FakeRoomListService = FakeRoomListService(), roomListRoomSummaryFactory: RoomListRoomSummaryFactory = aRoomListRoomSummaryFactory(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), dateTimeObserver: FakeDateTimeObserver = FakeDateTimeObserver(), + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ) = RoomListDataSource( roomListService = roomListService, roomListRoomSummaryFactory = roomListRoomSummaryFactory, @@ -112,6 +273,6 @@ class RoomListDataSourceTest { notificationSettingsService = notificationSettingsService, sessionCoroutineScope = backgroundScope, dateTimeObserver = dateTimeObserver, - analyticsService = FakeAnalyticsService(), + analyticsService = analyticsService, ) } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt index 4c361b47f3..6d68491ad1 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersViewTest.kt @@ -6,34 +6,32 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.filters import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.home.impl.R import io.element.android.features.home.impl.filters.selection.FilterSelectionState import io.element.android.libraries.testtags.TestTags import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressTag -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomListFiltersViewTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomListFiltersViewTest : RobolectricTest() { @Test - fun `clicking on filters generates expected Event`() { + fun `clicking on filters generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { RoomListFiltersView( state = aRoomListFiltersState(eventSink = eventsRecorder), ) } - rule.clickOn(R.string.screen_roomlist_filter_rooms) + clickOn(R.string.screen_roomlist_filter_rooms) eventsRecorder.assertList( listOf( RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms), @@ -42,9 +40,9 @@ class RoomListFiltersViewTest { } @Test - fun `clicking on clear filters generates expected Event`() { + fun `clicking on clear filters generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { RoomListFiltersView( state = aRoomListFiltersState( filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) }, @@ -52,7 +50,7 @@ class RoomListFiltersViewTest { ), ) } - rule.pressTag(TestTags.homeScreenClearFilters.value) + pressTag(TestTags.homeScreenClearFilters.value) eventsRecorder.assertList( listOf( RoomListFiltersEvent.ClearSelectedFilters, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt index 6be5fe4c16..9ebad7f7f6 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenuTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.roomlist 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.home.impl.R import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings @@ -19,24 +21,20 @@ import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomListContextMenuTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomListContextMenuTest : RobolectricTest() { @Test - fun `clicking on Mark as read generates expected Events`() { + fun `clicking on Mark as read generates expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = true) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_roomlist_mark_as_read) + clickOn(R.string.screen_roomlist_mark_as_read) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -46,14 +44,14 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Mark as unread generates expected Events`() { + fun `clicking on Mark as unread generates expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(hasNewContent = false) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_roomlist_mark_as_unread) + clickOn(R.string.screen_roomlist_mark_as_unread) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -63,14 +61,14 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Leave room generates expected Events`() { + fun `clicking on Leave room generates expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, ) - rule.clickOn(CommonStrings.action_leave_room) + clickOn(CommonStrings.action_leave_room) eventsRecorder.assertList( listOf( RoomListEvent.HideContextMenu, @@ -80,48 +78,48 @@ class RoomListContextMenuTest { } @Test - fun `clicking on Report room invokes the expected callback and generates expected Event`() { + fun `clicking on Report room invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, canReportRoom = true, eventSink = eventsRecorder, onRoomSettingsClick = EnsureNeverCalledWithParam(), onReportRoomClick = callback, ) - rule.clickOn(CommonStrings.action_report_room) + clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) callback.assertSuccess() } @Test - fun `clicking on Settings invokes the expected callback and generates expected Event`() { + fun `clicking on Settings invokes the expected callback and generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown() val callback = EnsureCalledOnceWithParam(contextMenu.roomId, Unit) - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, onRoomSettingsClick = callback, ) - rule.clickOn(CommonStrings.common_settings) + clickOn(CommonStrings.common_settings) eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) callback.assertSuccess() } @Test - fun `clicking on Favourites generates expected Event`() { + fun `clicking on Favourites generates expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val contextMenu = aContextMenuShown(isDm = false, isFavorite = false) val callback = EnsureNeverCalledWithParam() - rule.setRoomListContextMenu( + setRoomListContextMenu( contextMenu = contextMenu, eventSink = eventsRecorder, onRoomSettingsClick = callback, ) - rule.clickOn(CommonStrings.common_favourite) + clickOn(CommonStrings.common_favourite) eventsRecorder.assertList( listOf( RoomListEvent.SetRoomIsFavorite(contextMenu.roomId, true), @@ -129,7 +127,7 @@ class RoomListContextMenuTest { ) } - private fun AndroidComposeTestRule<*, *>.setRoomListContextMenu( + private fun AndroidComposeUiTest.setRoomListContextMenu( contextMenu: RoomListState.ContextMenu.Shown, canReportRoom: Boolean = false, eventSink: (RoomListEvent) -> Unit, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt index d7f509fda4..cb0226353b 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenuTest.kt @@ -6,31 +6,28 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.roomlist -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.home.impl.model.aRoomListRoomSummary import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomListDeclineInviteMenuTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomListDeclineInviteMenuTest : RobolectricTest() { @Test - fun `clicking on decline emits the expected Events`() { + fun `clicking on decline emits the expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -38,7 +35,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertList( listOf( RoomListEvent.HideDeclineInviteMenu, @@ -48,10 +45,10 @@ class RoomListDeclineInviteMenuTest { } @Test - fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() { + fun `clicking on decline and block when canReportRoom=true, it emits the expected Events and callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = true, @@ -59,16 +56,16 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_decline_and_block) + clickOn(CommonStrings.action_decline_and_block) val expectedEvents = listOf(RoomListEvent.HideDeclineInviteMenu) eventsRecorder.assertList(expectedEvents) } @Test - fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() { + fun `clicking on decline and block when canReportRoom=false, it emits the expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -76,7 +73,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_decline_and_block) + clickOn(CommonStrings.action_decline_and_block) val expectedEvents = listOf( RoomListEvent.HideDeclineInviteMenu, RoomListEvent.DeclineInvite(menu.roomSummary, blockUser = true), @@ -85,10 +82,10 @@ class RoomListDeclineInviteMenuTest { } @Test - fun `clicking on cancel emits the expected Event`() { + fun `clicking on cancel emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val menu = RoomListState.DeclineInviteMenu.Shown(roomSummary = aRoomListRoomSummary()) - rule.setSafeContent { + setSafeContent { RoomListDeclineInviteMenu( menu = menu, canReportRoom = false, @@ -96,7 +93,7 @@ class RoomListDeclineInviteMenuTest { eventSink = eventsRecorder, ) } - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertList(listOf(RoomListEvent.HideDeclineInviteMenu)) } } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 99a5550a08..642fd462d7 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId @@ -61,9 +63,7 @@ import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore -import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.libraries.push.api.battery.aBatteryOptimizationState import io.element.android.libraries.push.api.notifications.NotificationCleaner @@ -89,6 +89,7 @@ import org.junit.Rule import org.junit.Test import kotlin.time.Duration.Companion.seconds +@Suppress("LargeClass") class RoomListPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -227,7 +228,6 @@ class RoomListPresenterTest { isDm = false, isFavorite = false, hasNewContent = false, - displayClearRoomCacheAction = false, ) ) } @@ -244,35 +244,6 @@ class RoomListPresenterTest { isDm = false, isFavorite = true, hasNewContent = false, - displayClearRoomCacheAction = false, - ) - ) - } - } - } - - @Test - fun `present - show context menu with view source on`() = runTest { - val presenter = createRoomListPresenter( - appPreferencesStore = InMemoryAppPreferencesStore( - isDeveloperModeEnabled = true, - ) - ) - presenter.test { - val initialState = awaitItem() - val summary = createRoomListRoomSummary() - initialState.eventSink(RoomListEvent.ShowContextMenu(summary)) - awaitItem().also { state -> - assertThat(state.contextMenu) - .isEqualTo( - RoomListState.ContextMenu.Shown( - roomId = summary.roomId, - roomName = summary.name, - isDm = false, - isFavorite = false, - // true here. - hasNewContent = false, - displayClearRoomCacheAction = true, ) ) } @@ -300,7 +271,6 @@ class RoomListPresenterTest { isDm = false, isFavorite = false, hasNewContent = false, - displayClearRoomCacheAction = false, ) ) @@ -665,9 +635,9 @@ class RoomListPresenterTest { spaceFiltersPresenter: Presenter = Presenter { aDisabledSpaceFiltersState() }, acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(), announcementService: AnnouncementService = FakeAnnouncementService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), ) = RoomListPresenter( client = client, leaveRoomPresenter = { leaveRoomState }, @@ -692,9 +662,9 @@ class RoomListPresenterTest { fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, batteryOptimizationPresenter = { aBatteryOptimizationState() }, notificationCleaner = notificationCleaner, - appPreferencesStore = appPreferencesStore, seenInvitesStore = seenInvitesStore, announcementService = announcementService, coldStartWatcher = FakeAnalyticsColdStartWatcher(), + featureFlagService = featureFlagService, ) } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt index 8402a921ca..773f428917 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt @@ -6,17 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.roomlist import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.home.impl.HomeView import io.element.android.features.home.impl.R import io.element.android.features.home.impl.aHomeState @@ -31,23 +33,17 @@ 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.ensureCalledOnceWithParam +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -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 RoomListViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class RoomListViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test - fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() { + fun `displaying the view automatically sends a couple of UpdateVisibleRangeEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -62,9 +58,9 @@ class RoomListViewTest { } @Test - fun `clicking on close recovery key banner emits the expected Event`() { + fun `clicking on close recovery key banner emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -74,15 +70,15 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() + val close = activity!!.getString(CommonStrings.action_close) + onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(RoomListEvent.DismissBanner) } @Test - fun `clicking on close setup key banner emits the expected Event`() { + fun `clicking on close setup key banner emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), eventSink = eventsRecorder, @@ -92,16 +88,16 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() + val close = activity!!.getString(CommonStrings.action_close) + onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(RoomListEvent.DismissBanner) } @Test - fun `clicking on continue recovery key banner invokes the expected callback`() { + fun `clicking on continue recovery key banner invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { callback -> - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, @@ -112,17 +108,17 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertEmpty() } } @Test - fun `clicking on continue setup key banner invokes the expected callback`() { + fun `clicking on continue setup key banner invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { callback -> - rule.setRoomListView( + setRoomListView( state = aRoomListState( contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery), eventSink = eventsRecorder, @@ -131,28 +127,28 @@ class RoomListViewTest { ) // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(R.string.banner_set_up_recovery_submit) + clickOn(R.string.banner_set_up_recovery_submit) eventsRecorder.assertEmpty() } } @Test - fun `clicking on start chat when the session has no room invokes the expected callback`() { + fun `clicking on start chat when the session has no room invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setRoomListView( + setRoomListView( state = aRoomListState( eventSink = eventsRecorder, contentState = anEmptyContentState(), ), onCreateRoomClick = callback, ) - rule.clickOn(CommonStrings.action_start_chat) + clickOn(CommonStrings.action_start_chat) } } @Test - fun `clicking on a room invokes the expected callback`() { + fun `clicking on a room invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -161,7 +157,7 @@ class RoomListViewTest { it.displayType == RoomSummaryDisplayType.ROOM } ensureCalledOnceWithParam(room0.roomId) { callback -> - rule.setRoomListView( + setRoomListView( state = state, onRoomClick = callback, ) @@ -169,14 +165,14 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent.content().toString()).performClick() + onNodeWithText(room0.latestEvent.content().toString()).performClick() } eventsRecorder.assertEmpty() } @Test - fun `clicking on a room twice invokes the expected callback only once`() { + fun `clicking on a room twice invokes the expected callback only once`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -185,13 +181,13 @@ class RoomListViewTest { it.displayType == RoomSummaryDisplayType.ROOM } ensureCalledOnceWithParam(room0.roomId) { callback -> - rule.setRoomListView( + setRoomListView( state = state, onRoomClick = callback, ) // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent.content().toString()) + onNodeWithText(room0.latestEvent.content().toString()) .performClick() .performClick() } @@ -199,7 +195,7 @@ class RoomListViewTest { } @Test - fun `long clicking on a room emits the expected Event`() { + fun `long clicking on a room emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -207,18 +203,18 @@ class RoomListViewTest { val room0 = state.contentAsRooms().summaries.first { it.displayType == RoomSummaryDisplayType.ROOM } - rule.setRoomListView( + setRoomListView( state = state, ) // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } + onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } eventsRecorder.assertSingle(RoomListEvent.ShowContextMenu(room0)) } @Test - fun `clicking on a room setting invokes the expected callback and emits expected Event`() { + fun `clicking on a room setting invokes the expected callback and emits expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( contextMenu = aContextMenuShown(), @@ -226,7 +222,7 @@ class RoomListViewTest { ) val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId ensureCalledOnceWithParam(room0) { callback -> - rule.setRoomListView( + setRoomListView( state = state, onRoomSettingsClick = callback, ) @@ -234,14 +230,14 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(CommonStrings.common_settings) + clickOn(CommonStrings.common_settings) } eventsRecorder.assertSingle(RoomListEvent.HideContextMenu) } @Test - fun `clicking on accept and decline invite emits the expected Events`() { + fun `clicking on accept and decline invite emits the expected Events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomListState( eventSink = eventsRecorder, @@ -249,13 +245,13 @@ class RoomListViewTest { val invitedRoom = state.contentAsRooms().summaries.first { it.displayType == RoomSummaryDisplayType.INVITE } - rule.setRoomListView(state = state) + setRoomListView(state = state) // Remove automatic initial events eventsRecorder.clear() - rule.clickOn(CommonStrings.action_accept) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_decline) eventsRecorder.assertList( listOf( RoomListEvent.AcceptInvite(invitedRoom), @@ -265,7 +261,7 @@ class RoomListViewTest { } } -private fun AndroidComposeTestRule.setRoomListView( +private fun AndroidComposeUiTest.setRoomListView( state: RoomListState, onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onSettingsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt index 5c1325b107..9c9d13d066 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt @@ -5,34 +5,30 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.home.impl.spacefilters import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.tests.testutils.EventsRecorder -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SpaceFiltersViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class SpaceFiltersViewTest : RobolectricTest() { @Test - fun `clicking on a filter with alias shows display name and alias`() { + fun `clicking on a filter with alias shows display name and alias`() = runAndroidComposeUiTest { val filter = aSpaceServiceFilter( displayName = "Test Space", canonicalAlias = A_ROOM_ALIAS, ) val eventsRecorder = EventsRecorder() - rule.setSpaceFiltersView( + setSpaceFiltersView( state = aSelectingSpaceFiltersState( availableFilters = listOf(filter), eventSink = eventsRecorder, @@ -40,20 +36,20 @@ class SpaceFiltersViewTest { ) // Both display name and alias should be visible - rule.onNodeWithText(filter.spaceRoom.displayName).assertExists() - rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists() + onNodeWithText(filter.spaceRoom.displayName).assertExists() + onNodeWithText(A_ROOM_ALIAS.value).assertExists() - rule.onNodeWithText(filter.spaceRoom.displayName).performClick() + onNodeWithText(filter.spaceRoom.displayName).performClick() eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter)) } @Test - fun `multiple filters are displayed and clickable`() { + fun `multiple filters are displayed and clickable`() = runAndroidComposeUiTest { val filter1 = aSpaceServiceFilter(displayName = "Space One") val filter2 = aSpaceServiceFilter(displayName = "Space Two") val eventsRecorder = EventsRecorder() - rule.setSpaceFiltersView( + setSpaceFiltersView( state = aSelectingSpaceFiltersState( availableFilters = listOf(filter1, filter2), eventSink = eventsRecorder, @@ -61,17 +57,17 @@ class SpaceFiltersViewTest { ) // Both filters should be visible - rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists() - rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists() + onNodeWithText(filter1.spaceRoom.displayName).assertExists() + onNodeWithText(filter2.spaceRoom.displayName).assertExists() // Click on second filter - rule.onNodeWithText(filter2.spaceRoom.displayName).performClick() + onNodeWithText(filter2.spaceRoom.displayName).performClick() eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2)) } } -private fun AndroidComposeTestRule.setSpaceFiltersView( +private fun AndroidComposeUiTest.setSpaceFiltersView( state: SpaceFiltersState, ) { setContent { diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt index 696e02a0d7..8bfea2c12c 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/InviteData.kt @@ -11,7 +11,6 @@ package io.element.android.features.invite.api import android.os.Parcelable import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomInfo -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.parcelize.Parcelize diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts index 80b98464f7..e033f2740c 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.services.analytics.api) diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt index 3f8bf93afa..db2e76c1c3 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt @@ -15,6 +15,7 @@ import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInv import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.invite.impl.AcceptInvite import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.preview.ROOM_NAME import io.element.android.libraries.matrix.api.core.RoomId open class AcceptDeclineInviteStateProvider : PreviewParameterProvider { @@ -26,7 +27,7 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider + + "No veuràs cap missatge ni invitacions a sales d\'aquest usuari." + "Bloqueja usuari" + "Denuncia aquesta sala al teu proveïdor de compte." + "Descriu el motiu de la denúncia…" + "Rebutja i bloqueja" + "Segur que vols rebutjar la invitació per unir-te a %1$s?" + "Rebutja invitació" + "Segur que vols rebutjar el xat privat amb %1$s?" + "Rebutja xat" + "Sense invitacions" + "%1$s (%2$s) t\'ha convidat" + "Sí, rebutja i bloqueja" + "Segur que vols rebutjar la invitació d\'unió a aquesta sala? Això també evitarà que %1$s et contacti i et convidi a sales." + "Rebutja la invitació i bloqueja" + "Rebutja i bloqueja" + diff --git a/features/invite/impl/src/main/res/values-ru/translations.xml b/features/invite/impl/src/main/res/values-ru/translations.xml index 4e1104266e..8139beb885 100644 --- a/features/invite/impl/src/main/res/values-ru/translations.xml +++ b/features/invite/impl/src/main/res/values-ru/translations.xml @@ -10,7 +10,7 @@ "Вы уверены, что хотите отказаться от личного общения с %1$s?" "Отклонить чат" "Нет приглашений" - "%1$s (%2$s) пригласил(а) вас" + "%1$s (%2$s) пригласил(а) Вас" "Да, отклонить и заблокировать" "Вы действительно хотите отклонить приглашение в комнату? Также %1$s больше не сможет связаться с вами или приглашать в комнаты." "Отклонить приглашение и заблокировать" 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 61e7f15f59..02825bca6d 100644 --- a/features/invite/impl/src/main/res/values-zh/translations.xml +++ b/features/invite/impl/src/main/res/values-zh/translations.xml @@ -10,7 +10,7 @@ "你确定要拒绝与 %1$s 私聊?" "拒绝聊天" "没有邀请" - "%1$s (%2$s)邀请了你" + "%1$s(%2$s)邀请了你" "是,拒绝并屏蔽" "你确定要拒绝此房间的加入邀请?这也将阻止 %1$s 与你联系或邀请你加入房间。" "拒绝邀请并屏蔽" diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt index 299fec8565..55c31a09d2 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.invite.impl.declineandblock import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.invite.impl.R import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -21,98 +23,93 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class DeclineAndBlockViewTest { - @get:Rule val rule = createAndroidComposeRule() +class DeclineAndBlockViewTest : RobolectricTest() { @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on decline when enabled emits the expected event`() { + fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( blockUser = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline) } @Test - fun `clicking on decline when disabled does not emit event`() { + fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( blockUser = false, reportRoom = false, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertEmpty() } @Test - fun `clicking on block option emits the expected event`() { + fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( blockUser = true, eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_decline_and_block_block_user_option_title) + clickOn(R.string.screen_decline_and_block_block_user_option_title) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser) } @Test - fun `clicking on report room option emits the expected event`() { + fun `clicking on report room option emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( reportRoom = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_report_room) + clickOn(CommonStrings.action_report_room) eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom) } @Test - fun `typing text in the reason field emits the expected Event`() { + fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeclineAndBlockView( + setDeclineAndBlockView( aDeclineAndBlockState( reportRoom = true, reportReason = "", eventSink = eventsRecorder, ), ) - rule.onNodeWithText("").performTextInput("Spam!") + onNodeWithText("").performTextInput("Spam!") eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!")) } } -private fun AndroidComposeTestRule.setDeclineAndBlockView( +private fun AndroidComposeUiTest.setDeclineAndBlockView( state: DeclineAndBlockState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts index 2df267f155..080ed765bb 100644 --- a/features/invite/test/build.gradle.kts +++ b/features/invite/test/build.gradle.kts @@ -16,6 +16,7 @@ android { dependencies { implementation(libs.coroutines.core) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.test) implementation(projects.tests.testutils) diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt index 264aafd570..0422fac4f1 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt @@ -11,4 +11,5 @@ package io.element.android.features.invitepeople.api interface InvitePeopleEvents { data object SendInvites : InvitePeopleEvents data object CloseSearch : InvitePeopleEvents + data object ClearError : InvitePeopleEvents } diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt index 9d342d191f..d14042cff7 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt @@ -9,10 +9,12 @@ package io.element.android.features.invitepeople.api import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId interface InvitePeopleState { val canInvite: Boolean val isSearchActive: Boolean val sendInvitesAction: AsyncAction + val createRoomFromDmAction: AsyncAction val eventSink: (InvitePeopleEvents) -> Unit } diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt index ce30bcc1f6..b233ed07ef 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.features.invitepeople.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId class InvitePeopleStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,6 +26,7 @@ private data class PreviewInvitePeopleState( override val canInvite: Boolean, override val isSearchActive: Boolean, override val sendInvitesAction: AsyncAction, + override val createRoomFromDmAction: AsyncAction, override val eventSink: (InvitePeopleEvents) -> Unit, ) : InvitePeopleState @@ -32,10 +34,12 @@ private fun aPreviewInvitePeopleState( canInvite: Boolean = false, isSearchActive: Boolean = false, sendInvitesAction: AsyncAction = AsyncAction.Uninitialized, + createRoomFromDmAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (InvitePeopleEvents) -> Unit = {}, ) = PreviewInvitePeopleState( canInvite = canInvite, isSearchActive = isSearchActive, sendInvitesAction = sendInvitesAction, + createRoomFromDmAction = createRoomFromDmAction, eventSink = eventSink ) diff --git a/features/invitepeople/impl/build.gradle.kts b/features/invitepeople/impl/build.gradle.kts index 185497c25b..2ab2fcb4a3 100644 --- a/features/invitepeople/impl/build.gradle.kts +++ b/features/invitepeople/impl/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.libraries.uiUtils) implementation(projects.libraries.androidutils) implementation(projects.libraries.usersearch.api) + implementation(projects.libraries.testtags) implementation(libs.coil.compose) implementation(projects.services.apperror.api) implementation(projects.libraries.featureflag.api) 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 b223d30617..44627daca3 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 @@ -38,12 +38,17 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.createroom.RoomPreset 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 import io.element.android.libraries.matrix.api.room.filterMembers +import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms +import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.usersearch.api.UserRepository @@ -88,6 +93,7 @@ class DefaultInvitePeoplePresenter( var searchActive by rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) } val sendInvitesAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + val createRoomFromDmAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val recentDirectRooms by produceState(emptyList(), roomMembers.value) { if (roomMembers.value.isSuccess()) { @@ -208,7 +214,13 @@ class DefaultInvitePeoplePresenter( ) } else { room.dataOrNull()?.let { - sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction) + sessionCoroutineScope.launch { + if (it.isDm()) { + createRoomFromDm(it, selectedUsers.value, createRoomFromDmAction) + } else { + sendInvites(it, selectedUsers.value, sendInvitesAction) + } + } } } } @@ -216,6 +228,10 @@ class DefaultInvitePeoplePresenter( searchActive = false queryState.clearText() } + is InvitePeopleEvents.ClearError -> { + sendInvitesAction.value = AsyncAction.Uninitialized + createRoomFromDmAction.value = AsyncAction.Uninitialized + } } } @@ -228,6 +244,7 @@ class DefaultInvitePeoplePresenter( searchResults = searchResults.value, showSearchLoader = showSearchLoader.value, sendInvitesAction = sendInvitesAction.value, + createRoomFromDmAction = createRoomFromDmAction.value, suggestions = suggestions, eventSink = ::handleEvent, ) @@ -254,6 +271,35 @@ class DefaultInvitePeoplePresenter( } } + private fun CoroutineScope.createRoomFromDm( + currentRoom: JoinedRoom, + selectedUsers: List, + createRoomFromDmAction: MutableState>, + ) = launch { + createRoomFromDmAction.runUpdatingState { + val currentUsers = currentRoom.getMembers(limit = 100).getOrNull().orEmpty() + .filter { it.membership.isActive() } + val invitees = (currentUsers.map { it.userId } + selectedUsers.map { it.userId }) + .filter { it != matrixClient.sessionId } + .distinct() + matrixClient.createRoom( + CreateRoomParameters( + name = null, + topic = null, + isEncrypted = true, + isDirect = false, + visibility = RoomVisibility.Private, + preset = RoomPreset.PRIVATE_CHAT, + invite = invitees, + avatar = null, + joinRuleOverride = JoinRule.Invite, + historyVisibilityOverride = RoomHistoryVisibility.Invited, + isSpace = false, + ) + ) + } + } + @JvmName("toggleUserInSelectedUsers") private fun MutableState>.toggleUser(user: MatrixUser) { value = if (value.contains(user)) { diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt index 842bcf1148..46e5d9f1a5 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt @@ -14,6 +14,7 @@ import io.element.android.features.invitepeople.api.InvitePeopleState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData 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 kotlinx.collections.immutable.ImmutableList @@ -26,6 +27,7 @@ data class DefaultInvitePeopleState( val selectedUsers: ImmutableList, override val isSearchActive: Boolean, override val sendInvitesAction: AsyncAction, + override val createRoomFromDmAction: AsyncAction, val suggestions: ImmutableList, override val eventSink: (InvitePeopleEvents) -> Unit ) : InvitePeopleState 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 c26b8de254..93a6e03bd3 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 @@ -12,7 +12,13 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL +import io.element.android.libraries.designsystem.preview.USER_NAME_EVE +import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN 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.matrix.ui.components.aMatrixUserList @@ -33,15 +39,15 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, + createRoomFromDmAction: AsyncAction = AsyncAction.Uninitialized, suggestions: List = aMatrixUserList() .take(5) .map { user -> anInvitableUser(matrixUser = user, isSelected = user in selectedUsers) }, @@ -125,6 +134,7 @@ private fun aDefaultInvitePeopleState( isSearchActive = isSearchActive, showSearchLoader = showSearchLoader, sendInvitesAction = sendInvitesAction, + createRoomFromDmAction = createRoomFromDmAction, suggestions = suggestions.toImmutableList(), eventSink = {}, ) diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt index b1f139f43c..2bbd64c977 100644 --- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt +++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt @@ -14,6 +14,7 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -23,6 +24,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -54,6 +56,8 @@ import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.strings.simplePluralStringResource import kotlinx.collections.immutable.ImmutableList @@ -102,7 +106,7 @@ private fun InvitePeopleContentView( } InvitePeopleSearchBar( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.imePadding().fillMaxWidth(), queryState = state.searchQuery, showLoader = state.showSearchLoader, selectedUsers = state.selectedUsers, @@ -298,7 +302,7 @@ private fun InvitePeopleConfirmModal( text = stringResource(CommonStrings.action_remove), onClick = onRemove, leadingIcon = IconSource.Vector(CompoundIcons.Close()), - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f).testTag(TestTags.confirmInviteUnknown), ) Button( text = stringResource(CommonStrings.action_invite), diff --git a/features/invitepeople/impl/src/main/res/values-ca/translations.xml b/features/invitepeople/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..294d04b4a3 --- /dev/null +++ b/features/invitepeople/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,5 @@ + + + "Ja és membre" + "Ja s\'ha convidat" + diff --git a/features/invitepeople/impl/src/main/res/values-da/translations.xml b/features/invitepeople/impl/src/main/res/values-da/translations.xml index fbb1814e9f..1754ef6e0d 100644 --- a/features/invitepeople/impl/src/main/res/values-da/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-da/translations.xml @@ -2,4 +2,8 @@ "Allerede medlem" "Allerede inviteret" + "Du har i øjeblikket ingen chats med disse kontakter. Bekræft deres invitation til dette rum, før du fortsætter." + "Du har i øjeblikket ingen chats med denne kontakt. Bekræft deres invitation til dette rum, før du fortsætter." + "Inviter nye kontakter til dette rum?" + "Inviter ny kontakt til dette rum?" diff --git a/features/invitepeople/impl/src/main/res/values-et/translations.xml b/features/invitepeople/impl/src/main/res/values-et/translations.xml index 44484d23c3..1bdf5f780c 100644 --- a/features/invitepeople/impl/src/main/res/values-et/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-et/translations.xml @@ -2,4 +2,8 @@ "Sa juba oled jututoa liige" "Sa juba oled kutse saanud" + "Sul pole hetkel nende kontaktidega ühtegi vestlust. Enne jätkamist kinnita neile siia jututuppa kutse saatmine." + "Sul pole hetkel selle kontaktiga ühtegi vestlust. Enne jätkamist kinnita talle siia jututuppa kutse saatmine." + "Kas kutsud uued kontaktid siia jututuppa?" + "Kas kutsud uue kontakti siia jututuppa?" diff --git a/features/invitepeople/impl/src/main/res/values-fi/translations.xml b/features/invitepeople/impl/src/main/res/values-fi/translations.xml index e347919719..d2283c7b2a 100644 --- a/features/invitepeople/impl/src/main/res/values-fi/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-fi/translations.xml @@ -2,4 +2,8 @@ "On jo jäsen" "On jo kutsuttu" + "Sinulla ei ole tällä hetkellä keskusteluja näiden yhteystietojen kanssa. Vahvista kutsusi heille tähän huoneeseen ennen kuin jatkat." + "Sinulla ei ole tällä hetkellä keskusteluja tämän yhteystiedon kanssa. Vahvista kutsusi hänelle tähän huoneeseen ennen kuin jatkat." + "Kutsutaanko uusia yhteystietoja tähän huoneeseen?" + "Kutsutaanko uusi yhteystieto tähän huoneeseen?" diff --git a/features/invitepeople/impl/src/main/res/values-hr/translations.xml b/features/invitepeople/impl/src/main/res/values-hr/translations.xml index 66031c5fd7..471b79d709 100644 --- a/features/invitepeople/impl/src/main/res/values-hr/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-hr/translations.xml @@ -2,4 +2,8 @@ "Već je član" "Već je pozvan/a" + "Trenutno nemate razgovora s ovim kontaktima. Potvrdite da ih želite pozvati u ovu sobu prije nego što nastavite." + "Trenutno nemate razgovora s ovim kontaktom. Potvrdite da ste ga pozvali u ovu sobu prije nego što nastavite." + "Pozvati nove kontakte u ovu sobu?" + "Pozvati novi kontakt u ovu sobu?" diff --git a/features/invitepeople/impl/src/main/res/values-hu/translations.xml b/features/invitepeople/impl/src/main/res/values-hu/translations.xml index 16f35b018c..de2cab0d73 100644 --- a/features/invitepeople/impl/src/main/res/values-hu/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-hu/translations.xml @@ -2,4 +2,8 @@ "Már tag" "Már meghívták" + "Jelenleg nincsenek csevegései ezekkel a kapcsolatokkal. Mielőtt továbbmenne, erősítse meg, hogy meghívja őket ebbe a szobába." + "Jelenleg nincsenek beszélgetései ezzel a személlyel. A folytatás előtt erősítse meg, hogy meghívja ebbe a szobába." + "Új személyeket hív meg ebbe a szobába?" + "Új személyt hív meg ebbe a szobába?" diff --git a/features/invitepeople/impl/src/main/res/values-it/translations.xml b/features/invitepeople/impl/src/main/res/values-it/translations.xml index 979e42de1b..82dc6126f1 100644 --- a/features/invitepeople/impl/src/main/res/values-it/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-it/translations.xml @@ -2,4 +2,8 @@ "Già membro" "Già invitato" + "Al momento non hai conversazioni con questi contatti. Conferma di invitarli in questa stanza prima di continuare." + "Al momento non hai converszioni con questo contatto. Conferma di invitarlo in questa stanza prima di continuare." + "Invita nuovi contatti in questa stanza?" + "Invitare un nuovo contatto in questa stanza?" diff --git a/features/invitepeople/impl/src/main/res/values-ko/translations.xml b/features/invitepeople/impl/src/main/res/values-ko/translations.xml index 19214c5a79..421f6d5e02 100644 --- a/features/invitepeople/impl/src/main/res/values-ko/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-ko/translations.xml @@ -2,4 +2,8 @@ "이미 회원" "이미 초대됨" + "이 사용자들과의 대화 내역이 없습니다. 계속하려면 먼저 이 방으로의 초대를 확인해 주세요." + "이 사용자와의 대화 내역이 없습니다. 계속하려면 먼저 이 방으로의 초대를 확인해 주세요." + "이 방에 새 사용자를 초대할까요?" + "이 방에 새 사용자를 초대할까요?" diff --git a/features/invitepeople/impl/src/main/res/values-pl/translations.xml b/features/invitepeople/impl/src/main/res/values-pl/translations.xml index bfd537bb4b..3e8371c1ab 100644 --- a/features/invitepeople/impl/src/main/res/values-pl/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-pl/translations.xml @@ -2,4 +2,8 @@ "Jest już członkiem" "Już zaproszony" + "Obecnie nie prowadzisz żadnych czatów z tymi kontaktami. Potwierdź zaproszenie, zanim przejdziesz dalej." + "Obecnie nie posiadasz żadnych czatów z tym kontaktem. Potwierdź zaproszenie, zanim przejdziesz dalej." + "Zaprosić nowe kontakty do tego pokoju?" + "Zaprosić nowy kontakt do tego pokoju?" diff --git a/features/invitepeople/impl/src/main/res/values-ro/translations.xml b/features/invitepeople/impl/src/main/res/values-ro/translations.xml index f03be4b263..40189e3186 100644 --- a/features/invitepeople/impl/src/main/res/values-ro/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-ro/translations.xml @@ -2,4 +2,8 @@ "Deja membru" "Deja invitat" + "În prezent, nu aveți nicio conversație cu aceste contacte. Confirmați invitarea lor în această cameră înainte de a continua." + "În prezent, nu aveți nicio conversație cu acest contact. Confirmați invitarea acestuia în cameră înainte de a continua." + "Invitați contactele noi în această cameră?" + "Invitați contactul nou în această cameră?" diff --git a/features/invitepeople/impl/src/main/res/values-ru/translations.xml b/features/invitepeople/impl/src/main/res/values-ru/translations.xml index 45e650f081..651ec33626 100644 --- a/features/invitepeople/impl/src/main/res/values-ru/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-ru/translations.xml @@ -2,4 +2,8 @@ "Уже участник" "Уже приглашен(а)" + "У тебя пока нет чатов с этими контактами. Подтверди приглашение в эту комнату, прежде чем продолжить." + "У Вас пока нет чатов с этим контактом. Подтвердите, что это действительно тот контакт, которого Вы хотите пригласить, прежде чем продолжить." + "Пригласить новых участников в эту комнату?" + "Пригласить нового участника в эту комнату?" diff --git a/features/invitepeople/impl/src/main/res/values-uk/translations.xml b/features/invitepeople/impl/src/main/res/values-uk/translations.xml index da3ac9fe5b..b7bb3c95d9 100644 --- a/features/invitepeople/impl/src/main/res/values-uk/translations.xml +++ b/features/invitepeople/impl/src/main/res/values-uk/translations.xml @@ -2,4 +2,8 @@ "Уже учасник" "Уже запрошені" + "Наразі у вас немає чатів із цими контактами. Підтвердьте запрошення їх до цієї кімнати, перш ніж продовжувати." + "Наразі у вас немає чатів із цим контактом. Підтвердьте запрошення до цієї кімнати, перш ніж продовжувати." + "Запросити нових контактів до цієї кімнати?" + "Запросити нового контакта до цієї кімнати?" 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 e7fef11423..4e141b2c4d 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 @@ -320,7 +320,7 @@ internal class DefaultInvitePeoplePresenterTest { val initialState = awaitItemAsDefault() skipItems(1) - val selectedUser = aMatrixUser() + val selectedUser = aMatrixUser(displayName = "John Doe") initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser)) @@ -358,7 +358,7 @@ internal class DefaultInvitePeoplePresenterTest { val initialState = awaitItemAsDefault() skipItems(1) - val selectedUser = aMatrixUser() + val selectedUser = aMatrixUser(displayName = "John Doe") // Given a query is made initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query") @@ -540,7 +540,7 @@ internal class DefaultInvitePeoplePresenterTest { } @Test - fun `present - suggestions are loaded from recent direct rooms`() = runTest { + fun `present - suggestions are loaded from recent DM rooms`() = runTest { val dmRoomId = RoomId("!dm_room:server.org") val otherUserId = UserId("@frank:server.org") val matrixClient = FakeMatrixClient(sessionId = A_USER_ID).apply { @@ -554,7 +554,7 @@ internal class DefaultInvitePeoplePresenterTest { roomId = dmRoomId, initialRoomInfo = aRoomInfo( id = dmRoomId, - isDirect = true, + isDm = true, activeMembersCount = 2, currentUserMembership = CurrentUserMembership.JOINED, ), @@ -591,7 +591,7 @@ internal class DefaultInvitePeoplePresenterTest { roomId = dmRoomId, initialRoomInfo = aRoomInfo( id = dmRoomId, - isDirect = true, + isDm = true, activeMembersCount = 2, currentUserMembership = CurrentUserMembership.JOINED, ), @@ -831,6 +831,54 @@ internal class DefaultInvitePeoplePresenterTest { } } + @Test + fun `present - inviting someone to a DM creates a new room`() = runTest { + val alice = aMatrixUser("@alice:example.com") + + val matrixClient = FakeMatrixClient( + encryptionService = FakeEncryptionService( + getUserIdentityResult = lambdaRecorder { userId: UserId -> + Result.success(IdentityState.Pinned) + } + ) + ) + val presenter = createDefaultInvitePeoplePresenter( + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClient = matrixClient, + joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + initialRoomInfo = aRoomInfo(isDm = true), + getMembersResult = { Result.success(listOf(aRoomMember(userId = alice.userId, membership = RoomMembershipState.JOIN))) }, + ) + ) + ) + presenter.test { + val initialState = awaitItem() + skipItems(1) + + // We want to add a new user to a DM + initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice)) + + // And we send the invites + initialState.eventSink(InvitePeopleEvents.SendInvites) + + skipItems(1) + + awaitItemAsDefault().run { + assertThat(canInvite).isTrue() + assertThat(sendInvitesAction.isUninitialized()).isTrue() + // Inviting to a DM should trigger the creation of a new room + assertThat(createRoomFromDmAction.isLoading()).isTrue() + } + + awaitItemAsDefault().run { + assertThat(sendInvitesAction.isUninitialized()).isTrue() + // Once the room is created, the action should be successful + assertThat(createRoomFromDmAction.isSuccess()).isTrue() + } + } + } + private suspend fun FakeUserRepository.emitStateWithUsers( users: List, isSearching: Boolean = false diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index 1e685d3f5a..d257952209 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -44,7 +44,6 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipDetails import io.element.android.libraries.matrix.api.room.RoomType -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index 7e5142321a..4bfa741321 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -15,6 +15,9 @@ import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInvit import io.element.android.libraries.architecture.AsyncAction 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.ROOM_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB 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.RoomIdOrAlias @@ -134,8 +137,8 @@ open class JoinRoomStateProvider : PreviewParameterProvider { joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned( banSender = InviteSender( userId = UserId("@alice:domain"), - displayName = "Alice", - avatarData = AvatarData("alice", "Alice", size = AvatarSize.InviteSender), + displayName = USER_NAME_ALICE, + avatarData = AvatarData("alice", USER_NAME_ALICE, size = AvatarSize.InviteSender), membershipChangeReason = "spamming" ), reason = "spamming", @@ -222,7 +225,7 @@ fun aJoinRoomState( internal fun anInviteSender( userId: UserId = UserId("@bob:domain"), - displayName: String = "Bob", + displayName: String = USER_NAME_BOB, avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender), membershipChangeReason: String? = null, ) = InviteSender( @@ -234,7 +237,7 @@ internal fun anInviteSender( internal fun anInviteData( roomId: RoomId = A_ROOM_ID, - roomName: String = "Room name", + roomName: String = ROOM_NAME, isDm: Boolean = false, ) = InviteData( roomId = roomId, diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 35cfbb1594..977308178d 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -609,6 +609,7 @@ private fun JoinRoomTopBar( val roundedCornerShape = RoundedCornerShape(8.dp) val titleModifier = Modifier .clip(roundedCornerShape) + .semantics { heading() } if (contentState.name != null) { Row( modifier = titleModifier, @@ -621,10 +622,7 @@ private fun JoinRoomTopBar( ) Text( modifier = Modifier - .padding(horizontal = 8.dp) - .semantics { - heading() - }, + .padding(horizontal = 8.dp), text = contentState.name, style = ElementTheme.typography.fontBodyLgMedium, maxLines = 1, 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 f166186a1b..3f8e2bac2b 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 @@ + "Так, адмяніць" "Адхіліць і заблакіраваць" "Далучыцца" "Націсніце, каб далучыцца" diff --git a/features/joinroom/impl/src/main/res/values-ca/translations.xml b/features/joinroom/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..e26c33d40f --- /dev/null +++ b/features/joinroom/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,32 @@ + + + "Has estat bandejat per %1$s." + "T\'han bandejat" + "Motiu: %1$s." + "Cancel·la la sol·licitud" + "Sí, cancel·la" + "Segur que vols cancel·lar la teva sol·licitud d\'unió a aquesta sala?" + "Cancel·la la sol·licitud d\'unió" + "Sí, rebutja i bloqueja" + "Segur que vols rebutjar la invitació d\'unió a aquesta sala? Això també evitarà que %1$s et contacti i et convidi a sales." + "Rebutja la invitació i bloqueja" + "Rebutja i bloqueja" + "Ha fallat la unió" + "O t\'han de convidar per unir-te o hi pot haver restriccions d\'accés." + "Oblida" + "Per unir-te, necessites una invitació" + "Uneix-te" + "Pot ser que t\'hagin de convidar o hagis de ser membre d\'un espai per unir-t\'hi." + "Envia sol·licitud d\'unió" + "Missatge (opcional)" + "Rebràs una invitació per unir-te a la sala si la teva sol·licitud és acceptada." + "Sol·licitud d\'unió enviada" + "No s\'ha pogut mostrar la vista prèvia de la sala. Pot ser degut a problemes de xarxa o del servidor." + "No s\'ha pogut mostrar la vista prèvia de la sala" + "%1$s encara no admet els espais. Pots accedir-hi a través del navegador web." + "Espais encara no compatibles" + "Clic al botó següent i s\'avisarà a un administrador de sala. Podràs unir-te un cop t\'hagi aprovat." + "Per poder veure l\'historial de missatges has de ser un membre de la sala." + "Vols unir-te a aquesta sala?" + "Vista prèvia no disponible" + diff --git a/features/joinroom/impl/src/main/res/values-ru/translations.xml b/features/joinroom/impl/src/main/res/values-ru/translations.xml index 9add5f4790..eda089b5c2 100644 --- a/features/joinroom/impl/src/main/res/values-ru/translations.xml +++ b/features/joinroom/impl/src/main/res/values-ru/translations.xml @@ -1,6 +1,6 @@ - "%1$s заблокировал(а) вас в комнате." + "%1$s заблокировал(а) Вас в комнате." "Вы были заблокированы в комнате" "Причина: %1$s." "Отменить запрос" @@ -27,7 +27,7 @@ "Не удалось показать предпросмотр комнаты" "%1$s еще не поддерживает пространства. Вы можете получить к ним доступ в веб-версии." "Пространства пока не поддерживаются" - "Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения вы сможете присоединиться к обсуждению." + "Нажмите кнопку ниже и администратор комнаты получит уведомление. После одобрения Вы сможете присоединиться к беседе." "Вы должны быть участником этой комнаты, чтобы просмотреть историю сообщений." "Хотите присоединиться к этой комнате?" "Предпросмотр недоступен" diff --git a/features/joinroom/impl/src/main/res/values-uk/translations.xml b/features/joinroom/impl/src/main/res/values-uk/translations.xml index ec2a24950a..1a83a6ca8c 100644 --- a/features/joinroom/impl/src/main/res/values-uk/translations.xml +++ b/features/joinroom/impl/src/main/res/values-uk/translations.xml @@ -15,6 +15,7 @@ "Вам потрібно отримати запрошення, щоб приєднатися, інакше доступ може бути обмежений." "Забути" "Вам потрібне запрошення, щоб приєднатися" + "Запрошено користувачем" "Доєднатися" "Можливо, вам знадобиться отримати запрошення або стати учасником простору, щоб приєднатися." "Постукати, щоб приєднатися" diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt index 0a3b1ca3c6..0c510cacdb 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.joinroom.impl 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.test.anInviteData import io.element.android.libraries.architecture.AsyncAction @@ -26,116 +28,111 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class JoinRoomViewTest { - @get:Rule val rule = createAndroidComposeRule() +class JoinRoomViewTest : RobolectricTest() { @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on Join room on CanJoin room emits the expected Event`() { + fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_join_action) + clickOn(R.string.screen_join_room_join_action) eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom) } @Test - fun `clicking on Knock room on CanKnock room emits the expected Event`() { + fun `clicking on Knock room on CanKnock room emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), knockMessage = "Knock knock", eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_knock_action) + clickOn(R.string.screen_join_room_knock_action) eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom) } @Test - fun `clicking on closing Knock error emits the expected Event`() { + fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), knockAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `clicking on cancel knock request emit the expected Event`() { + fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_cancel_knock_action) + clickOn(R.string.screen_join_room_cancel_knock_action) eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true)) } @Test - fun `clicking on closing Cancel Knock error emits the expected Event`() { + fun `clicking on closing Cancel Knock error emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked), cancelKnockAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `clicking on closing Join error emits the expected Event`() { + fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), joinAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) } @Test - fun `when joining room is successful, the expected callback is invoked`() { + fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( joinAction = AsyncAction.Success(Unit), eventSink = eventsRecorder, @@ -146,53 +143,55 @@ class JoinRoomViewTest { } @Test - fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() { + fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData)) } @Test - fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() { + fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false)) } @Test fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - val inviteData = anInviteData() - val joinRoomState = aJoinRoomState( - contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), - canReportRoom = true, - eventSink = eventsRecorder, - ) - ensureCalledOnceWithParam(inviteData) { - rule.setJoinRoomView( - state = joinRoomState, - onDeclineInviteAndBlockUser = it, + runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + val inviteData = anInviteData() + val joinRoomState = aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())), + canReportRoom = true, + eventSink = eventsRecorder, ) - rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + ensureCalledOnceWithParam(inviteData) { + setJoinRoomView( + state = joinRoomState, + onDeclineInviteAndBlockUser = it, + ) + clickOn(R.string.screen_join_room_decline_and_block_button_title) + } } } @Test - fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() { + fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val inviteData = anInviteData() val joinRoomState = aJoinRoomState( @@ -200,29 +199,29 @@ class JoinRoomViewTest { canReportRoom = false, eventSink = eventsRecorder, ) - rule.setJoinRoomView(state = joinRoomState) - rule.clickOn(R.string.screen_join_room_decline_and_block_button_title) + setJoinRoomView(state = joinRoomState) + clickOn(R.string.screen_join_room_decline_and_block_button_title) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true)) } @Test - fun `clicking on Retry when an error occurs emits the expected Event`() { + fun `clicking on Retry when an error occurs emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aFailureContentState(), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent) } @Test - fun `clicking on ok when user is unauthorized the expected callback`() { + fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(), joinAction = AsyncAction.Failure(JoinRoom.Failures.UnauthorizedJoin), @@ -230,25 +229,25 @@ class JoinRoomViewTest { ), onBackClick = it ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) } } @Test - fun `clicking on forget when user is banned invokes the expected callback`() { + fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomView( + setJoinRoomView( aJoinRoomState( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_join_room_forget_action) + clickOn(R.string.screen_join_room_forget_action) eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom) } } -private fun AndroidComposeTestRule.setJoinRoomView( +private fun AndroidComposeUiTest.setJoinRoomView( state: JoinRoomState, onBackClick: () -> Unit = EnsureNeverCalled(), onJoinSuccess: () -> Unit = EnsureNeverCalled(), diff --git a/features/knockrequests/impl/build.gradle.kts b/features/knockrequests/impl/build.gradle.kts index 6f030479f5..e6a1a30167 100644 --- a/features/knockrequests/impl/build.gradle.kts +++ b/features/knockrequests/impl/build.gradle.kts @@ -33,9 +33,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) implementation(projects.libraries.designsystem) - implementation(projects.libraries.featureflag.api) testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.featureflag.test) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt index 67f1aaae8f..7210e783fe 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt @@ -11,6 +11,9 @@ package io.element.android.features.knockrequests.impl.banner import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CHARLIE import kotlinx.collections.immutable.toImmutableList class KnockRequestsBannerStateProvider : PreviewParameterProvider { @@ -29,15 +32,15 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider perms.knockRequestPermissions() }, - isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock), - coroutineScope = room.roomCoroutineScope + coroutineScope = room.roomCoroutineScope, ) } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt index 98570e6b28..00e0a30563 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -12,7 +12,6 @@ import io.element.android.features.knockrequests.api.KnockRequestPermissions import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.knock.KnockRequest -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -28,26 +27,20 @@ import kotlinx.coroutines.supervisorScope class KnockRequestsService( knockRequestsFlow: Flow>, permissionsFlow: Flow, - isKnockFeatureEnabledFlow: Flow, coroutineScope: CoroutineScope, ) { // Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them. private val handledKnockRequestIds = MutableStateFlow>(emptySet()) val knockRequestsFlow = combine( - isKnockFeatureEnabledFlow, knockRequestsFlow, handledKnockRequestIds, - ) { isKnockEnabled, knockRequests, handledKnockIds -> - if (!isKnockEnabled) { - AsyncData.Success(persistentListOf()) - } else { - val presentableKnockRequests = knockRequests - .filter { it.eventId !in handledKnockIds } - .map { inner -> KnockRequestWrapper(inner) } - .toImmutableList() - AsyncData.Success(presentableKnockRequests) - } + ) { knockRequests, handledKnockIds -> + val presentableKnockRequests = knockRequests + .filter { it.eventId !in handledKnockIds } + .map { inner -> KnockRequestWrapper(inner) } + .toImmutableList() + AsyncData.Success(presentableKnockRequests) }.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading()) val permissionsFlow = permissionsFlow.stateIn( diff --git a/features/knockrequests/impl/src/main/res/values-be/translations.xml b/features/knockrequests/impl/src/main/res/values-be/translations.xml index e6f08bc320..5fcb54e9af 100644 --- a/features/knockrequests/impl/src/main/res/values-be/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-be/translations.xml @@ -1,4 +1,6 @@ + "Адхіліць доступ" + "Паглядзець усе" "Прыняць" diff --git a/features/knockrequests/impl/src/main/res/values-ca/translations.xml b/features/knockrequests/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..bdf1cac21b --- /dev/null +++ b/features/knockrequests/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,35 @@ + + + "Sí, accepta-les totes" + "Segur que vols acceptar totes les sol·licituds d\'unió?" + "Accepta totes les sol·licituds" + "Accepta-les totes" + "No s\'han pogut acceptar totes les sol·licituds. Vols tornar-ho a intentar?" + "No s\'han pogut acceptar totes les sol·licituds" + "Acceptant totes les sol·licituds d\'unió" + "No s\'ha pogut acceptar la sol·licitud. Vols tornar-ho a intentar?" + "No s\'ha pogut acceptar la sol·licitud" + "Acceptant sol·licitud d\'unió" + "Sí, rebutja i bandeja" + "Segur que vols rebutjar i bandejar %1$s? L\'usuari no podrà sol·licitar de nou l\'accés d\'unió a aquesta sala." + "Rebutja i bandeja l\'accés" + "Rebutjant i bandejant l\'accés" + "Sí, rebutja" + "Segur que vols rebutjar %1$s d\'unir-se a aquesta sala?" + "Rebutja l\'accés" + "Rebutja i bandeja" + "No s\'ha pogut rebutjar la sol·licitud. Vols tornar-ho a intentar?" + "No s\'ha pogut rebutjar la sol·licitud" + "Rebutjant sol·licitud d\'unió" + "Quan algú demani unir-se a la sala, aquí podràs veure la sol·licitud." + "No hi ha sol·licituds d\'unió pendents" + "Carregant sol·licituds d\'unió…" + "Sol·licituds d\'unió" + + "%1$s +%2$d altre volen unir-se a la sala" + "%1$s +%2$d altres volen unir-se a la sala" + + "Veure totes" + "Accepta" + "%1$s vol unir-se a aquesta sala" + diff --git a/features/knockrequests/impl/src/main/res/values-ru/translations.xml b/features/knockrequests/impl/src/main/res/values-ru/translations.xml index 22d400cc50..ee3c31ebb3 100644 --- a/features/knockrequests/impl/src/main/res/values-ru/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-ru/translations.xml @@ -4,10 +4,10 @@ "Вы действительно хотите принять все заявки на присоединение?" "Принять все запросы" "Принять все" - "Мы не смогли принять все запросы. Попробовать еще раз?" + "Мы не смогли принять все запросы. Попробовать ещё раз?" "Не удалось принять все запросы" "Принять все заявки на присоединение" - "Мы не смогли принять этот запрос. Хотите попробовать еще раз?" + "Мы не смогли принять этот запрос. Хотите попробовать ещё раз?" "Не удалось принять запрос" "Принятие заявки на присоединение" "Да, отклонить и запретить" @@ -18,7 +18,7 @@ "Вы уверены, что хотите отклонить запрос %1$s на присоединение к этой комнате?" "Отклонить доступ" "Отклонить и запретить" - "Мы не смогли отклонить этот запрос. Попробовать еще раз?" + "Мы не смогли отклонить этот запрос. Попробовать ещё раз?" "Не удалось отклонить запрос" "Отклонение заявки на присоединение" "Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате." 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 d70632861c..d8b8f76b13 100644 --- a/features/knockrequests/impl/src/main/res/values-zh/translations.xml +++ b/features/knockrequests/impl/src/main/res/values-zh/translations.xml @@ -17,7 +17,7 @@ "是,拒绝" "你确定要拒绝 %1$s 加入此房间的申请?" "拒绝访问" - "拒绝和禁止" + "拒绝并封禁" "我们无法拒绝此申请。是否重试?" "拒绝申请失败" "拒绝加入申请" diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt index 3161d3e81f..1595fcdbd9 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt @@ -28,18 +28,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest { - @Test - fun `present - when feature is disabled then the banner should be hidden`() = runTest { - val knockRequests = flowOf(listOf(FakeKnockRequest())) - val presenter = createKnockRequestsBannerPresenter(isFeatureEnabled = false, knockRequestsFlow = knockRequests) - presenter.test { - skipItems(1) - awaitItem().also { state -> - assertThat(state.isVisible).isFalse() - } - } - } - @Test fun `present - when empty knock request list then the banner should be hidden`() = runTest { val knockRequests = flowOf(emptyList()) @@ -229,12 +217,10 @@ import org.junit.Test private fun TestScope.createKnockRequestsBannerPresenter( knockRequestsFlow: Flow> = flowOf(emptyList()), canAcceptKnockRequests: Boolean = true, - isFeatureEnabled: Boolean = true, ): KnockRequestsBannerPresenter { val knockRequestsService = KnockRequestsService( knockRequestsFlow = knockRequestsFlow, coroutineScope = backgroundScope, - isKnockFeatureEnabledFlow = flowOf(isFeatureEnabled), permissionsFlow = flowOf(KnockRequestPermissions(canAcceptKnockRequests, canAcceptKnockRequests, canAcceptKnockRequests)), ) return KnockRequestsBannerPresenter( diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt index a9fea0905e..f55ccb0cef 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.knockrequests.impl.banner import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import io.element.android.libraries.ui.strings.CommonStrings @@ -21,35 +23,29 @@ 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 org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class KnockRequestsBannerViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class KnockRequestsBannerViewTest : RobolectricTest() { @Test - fun `clicking on view on single request invoke the expected callback`() { + fun `clicking on view on single request invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), onViewRequestsClick = it ) - rule.clickOn(R.string.screen_room_single_knock_request_view_button_title) + clickOn(R.string.screen_room_single_knock_request_view_button_title) } } @Test - fun `clicking on view all when multiple requests invoke the expected callback`() { + fun `clicking on view all when multiple requests invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( knockRequests = listOf( aKnockRequestPresentable(displayName = "Alice"), @@ -60,37 +56,37 @@ class KnockRequestsBannerViewTest { ), onViewRequestsClick = it ) - rule.clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) + clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) } } @Test - fun `clicking on accept on a single request emit the expected event`() { + fun `clicking on accept on a single request emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest) } @Test - fun `clicking on dismiss emit the expected event`() { + fun `clicking on dismiss emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setKnockRequestsBannerView( + setKnockRequestsBannerView( state = aKnockRequestsBannerState( eventSink = eventsRecorder, ), ) - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() + val close = activity!!.getString(CommonStrings.action_close) + onNodeWithContentDescription(close).performClick() eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss) } } -private fun AndroidComposeTestRule.setKnockRequestsBannerView( +private fun AndroidComposeUiTest.setKnockRequestsBannerView( state: KnockRequestsBannerState, onViewRequestsClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt index 7102b01773..209e67cadf 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt @@ -298,7 +298,6 @@ internal fun TestScope.createKnockRequestsListPresenter( val knockRequestsService = KnockRequestsService( knockRequestsFlow = knockRequestsFlow, coroutineScope = backgroundScope, - isKnockFeatureEnabledFlow = flowOf(true), permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)), ) return KnockRequestsListPresenter(knockRequestsService = knockRequestsService) diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt index 188dcc7e56..1fbcb67b21 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.knockrequests.impl.list 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.knockrequests.impl.R import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import io.element.android.libraries.architecture.AsyncAction @@ -22,91 +24,86 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.persistentListOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class KnockRequestsListViewTest { - @get:Rule val rule = createAndroidComposeRule() +class KnockRequestsListViewTest : RobolectricTest() { @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on accept emit the expected event`() { + fun `clicking on accept emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest)) } @Test - fun `clicking on decline emit the expected event`() { + fun `clicking on decline emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest)) } @Test - fun `clicking on decline and ban emit the expected event`() { + fun `clicking on decline and ban emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) + clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest)) } @Test - fun `clicking on accept all emit the expected event`() { + fun `clicking on accept all emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_knock_requests_list_accept_all_button_title) + clickOn(R.string.screen_knock_requests_list_accept_all_button_title) eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll) } @Test - fun `retry on async view retry emit the expected event`() { + fun `retry on async view retry emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), @@ -114,15 +111,15 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction) } @Test - fun `canceling async view emit the expected event`() { + fun `canceling async view emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(RuntimeException("Failed to accept all")), @@ -130,15 +127,15 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction) } @Test - fun `confirming async view emit the expected event`() { + fun `confirming async view emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) - rule.setKnockRequestsListView( + setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.ConfirmingNoParams, @@ -146,12 +143,12 @@ class KnockRequestsListViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) + clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction) } } -private fun AndroidComposeTestRule.setKnockRequestsListView( +private fun AndroidComposeUiTest.setKnockRequestsListView( state: KnockRequestsListState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/leaveroom/api/src/main/res/values-ca/translations.xml b/features/leaveroom/api/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..870e0c35f2 --- /dev/null +++ b/features/leaveroom/api/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "Segur que vols sortir d\'aquest xat? El xat no és públic i no t\'hi podràs tornar a unir sense una invitació." + "Segur que vols sortir d\'aquesta sala? N\'ets l\'única persona. Si en surts, ningú s\'hi podrà unir i tu tampoc." + "Segur que vols sortir d\'aquesta sala? La sala no és pública i no podràs tornar a unir-t\'hi sense una invitació." + "Segur que vols sortir de la sala?" + diff --git a/features/leaveroom/api/src/main/res/values-fa/translations.xml b/features/leaveroom/api/src/main/res/values-fa/translations.xml index 167070891f..ec2cd89314 100644 --- a/features/leaveroom/api/src/main/res/values-fa/translations.xml +++ b/features/leaveroom/api/src/main/res/values-fa/translations.xml @@ -1,5 +1,6 @@ + "آیا مطمئنید که می‌خواهید این مکالمه را ترک کنید؟ این مکالمه عمومی نیست و بدون دعوت نمی‌توانید دوباره به آن بپیوندید." "مطمئنید که می‌خواهید این اتاق را ترک کنید؟ تنها فرد این‌جا هستید. در صورت ترک، هیچ‌کسی از جمله خودتان در آینده نخواهد توانست به آن بپیوندد." "مطمئنید که می‌خواهید این اتاق را ترک کنید؟ این اتاق عمومی نبوده قادر نخواهید بود بدون دعوت دوباره بپیوندید." "گزینش مالکان" diff --git a/features/leaveroom/api/src/main/res/values-ru/translations.xml b/features/leaveroom/api/src/main/res/values-ru/translations.xml index 898dd684b8..4fc11fccf4 100644 --- a/features/leaveroom/api/src/main/res/values-ru/translations.xml +++ b/features/leaveroom/api/src/main/res/values-ru/translations.xml @@ -1,7 +1,7 @@ - "Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и вы не сможете присоединиться к ней без приглашения." - "Вы уверены, что хотите покинуть эту комнату? Здесь есть только вы. Если вы покинете комнату, никто не сможет присоединиться к ней в будущем, включая вас." + "Вы уверены, что хотите покинуть беседу? Эта беседа не является общедоступной, и Вы не сможете снова присоединиться к ней без приглашения." + "Вы уверены, что хотите покинуть эту комнату? Здесь есть только Вы. Если Вы покинете комнату, никто не сможет присоединиться к ней в будущем, включая Вас." "Вы уверены, что хотите покинуть эту комнату? Эта комната не является общедоступной, и Вы не сможете присоединиться к ней без приглашения." "Назначить владельцев" "Вы единственный владелец этой комнаты. Перед тем, как её покинуть, необходимо передать владение кому-нибудь другому." diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt index 6455b45659..d11dc7e7e8 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt @@ -24,7 +24,6 @@ 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.BaseRoom import io.element.android.libraries.matrix.api.room.RoomMember -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import kotlinx.coroutines.CoroutineScope diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt index 59d2c1ce23..90b7f2369f 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt @@ -115,7 +115,7 @@ class LeaveBaseRoomPresenterTest { givenGetRoomResult( roomId = A_ROOM_ID, result = FakeBaseRoom().apply { - givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 2)) + givenRoomInfo(aRoomInfo(isDm = true, activeMembersCount = 2)) }, ) } diff --git a/features/linknewdevice/impl/build.gradle.kts b/features/linknewdevice/impl/build.gradle.kts index 9c1aa9e990..4659a2d2b8 100644 --- a/features/linknewdevice/impl/build.gradle.kts +++ b/features/linknewdevice/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { // TODO Cleanup implementation(projects.appconfig) implementation(projects.features.enterprise.api) + implementation(projects.features.lockscreen.api) implementation(projects.features.rageshake.api) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) @@ -43,7 +44,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.qrcode) - implementation(projects.libraries.oidc.api) + implementation(projects.libraries.oauth.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.wellknown.api) implementation(libs.androidx.browser) @@ -53,10 +54,11 @@ dependencies { testCommonDependencies(libs, true) testImplementation(projects.features.linknewdevice.test) + testImplementation(projects.features.lockscreen.test) testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.libraries.oidc.test) + testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.wellknown.test) diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt index 54baee6663..d686b22860 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt @@ -13,8 +13,10 @@ import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.PermanentChild import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack @@ -26,14 +28,18 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint +import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType import io.element.android.features.linknewdevice.impl.screens.number.EnterNumberNode import io.element.android.features.linknewdevice.impl.screens.qrcode.ShowQrCodeNode +import io.element.android.features.linknewdevice.impl.screens.root.LinkDeviceType import io.element.android.features.linknewdevice.impl.screens.root.LinkNewDeviceRootNode import io.element.android.features.linknewdevice.impl.screens.scan.ScanQrCodeNode +import io.element.android.features.lockscreen.api.DeviceUnlockEntryPoint import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode @@ -64,11 +70,17 @@ class LinkNewDeviceFlowNode( private val sessionCoroutineScope: CoroutineScope, private val linkNewMobileHandler: LinkNewMobileHandler, private val linkNewDesktopHandler: LinkNewDesktopHandler, + private val sessionEnterpriseService: SessionEnterpriseService, + private val deviceUnlockEntryPoint: DeviceUnlockEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap, ), + permanentNavModel = PermanentNavModel( + navTargets = setOf(NavTarget.LockScreen), + savedStateMap = buildContext.savedStateMap, + ), buildContext = buildContext, plugins = plugins, ) { @@ -107,6 +119,11 @@ class LinkNewDeviceFlowNode( val data: String, ) : NavTarget + @Parcelize + data class CodeConfirmation( + val code: String, + ) : NavTarget + @Parcelize data object MobileEnterNumber : NavTarget @@ -116,6 +133,9 @@ class LinkNewDeviceFlowNode( @Parcelize data object DesktopScanQrCode : NavTarget + @Parcelize + data object LockScreen : NavTarget + @Parcelize data class Error( val errorScreenType: ErrorScreenType, @@ -129,6 +149,9 @@ class LinkNewDeviceFlowNode( Timber.tag(tag.value).d("step: ${linkMobileStep::class.java.simpleName}") when (linkMobileStep) { LinkMobileStep.Uninitialized -> Unit + LinkMobileStep.CreatingQrCode -> { + // This step is handled in LinkNewDeviceRootPresenter + } LinkMobileStep.Done -> { callback.onDone() } @@ -136,8 +159,14 @@ class LinkNewDeviceFlowNode( navigateToError(linkMobileStep.errorType) } is LinkMobileStep.QrReady -> { - // The QrCode is ready, navigate to its display - backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data)) + // The QrCode is ready, navigate to its display, if not already there + val navTarget = backstack.elements.value.last().key.navTarget + if (navTarget !is NavTarget.MobileShowQrCode) { + backstack.push(NavTarget.MobileShowQrCode(linkMobileStep.data)) + } + } + LinkMobileStep.QrRotating -> { + // This step is handled in ShowQrCodePresenter } is LinkMobileStep.QrScanned -> { backstack.replace(NavTarget.MobileEnterNumber) @@ -145,10 +174,7 @@ class LinkNewDeviceFlowNode( LinkMobileStep.Starting -> { // This step is not received at the moment, so do nothing } - LinkMobileStep.SyncingSecrets -> { - // LinkMobileStep.Done is not received at the moment, so consider that the flow is done here - callback.onDone() - } + LinkMobileStep.SyncingSecrets -> Unit is LinkMobileStep.WaitingForAuth -> { navigateToBrowser(linkMobileStep.verificationUri) } @@ -166,7 +192,9 @@ class LinkNewDeviceFlowNode( is LinkDesktopStep.Error -> { navigateToError(linkDesktopStep.errorType) } - is LinkDesktopStep.EstablishingSecureChannel -> Unit + is LinkDesktopStep.EstablishingSecureChannel -> { + backstack.push(NavTarget.CodeConfirmation(linkDesktopStep.checkCodeString)) + } is LinkDesktopStep.InvalidQrCode -> { // This error will be handled by the ScanQrCodeNode } @@ -183,20 +211,20 @@ class LinkNewDeviceFlowNode( private fun navigateToError(errorType: ErrorType) { // Map the error to an error screen - // TODO Update this mapping val error = when (errorType) { - is ErrorType.DeviceIdAlreadyInUse -> ErrorScreenType.UnknownError - is ErrorType.InvalidCheckCode -> ErrorScreenType.InsecureChannelDetected - is ErrorType.MissingSecretsBackup -> ErrorScreenType.UnknownError - is ErrorType.NotFound -> ErrorScreenType.Expired - is ErrorType.DeviceNotFound -> ErrorScreenType.UnknownError - is ErrorType.Unknown -> ErrorScreenType.UnknownError - is ErrorType.UnsupportedProtocol -> ErrorScreenType.UnknownError - is ErrorType.Cancelled -> ErrorScreenType.UnknownError + is ErrorType.InvalidCheckCode -> ErrorScreenType.Mismatch2Digits + is ErrorType.UnsupportedProtocol -> ErrorScreenType.ProtocolNotSupported + is ErrorType.Cancelled -> ErrorScreenType.Cancelled is ErrorType.ConnectionInsecure -> ErrorScreenType.InsecureChannelDetected - is ErrorType.Expired -> ErrorScreenType.Expired - is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.UnknownError + is ErrorType.Expired, + is ErrorType.NotFound, + is ErrorType.DeviceNotFound -> ErrorScreenType.Expired + is ErrorType.OtherDeviceAlreadySignedIn -> ErrorScreenType.OtherDeviceAlreadySignedIn + // TODO check if we expect to hit this here or if it should be caught earlier on is ErrorType.UnsupportedQrCodeType -> ErrorScreenType.UnknownError + is ErrorType.MissingSecretsBackup, + is ErrorType.DeviceIdAlreadyInUse, + is ErrorType.Unknown -> ErrorScreenType.UnknownError } // It is OK to push on backstack, since when user leaves the error screen, a new root will be set, // or the whole flow will be popped. @@ -211,13 +239,28 @@ class LinkNewDeviceFlowNode( callback.onDone() } - override fun linkDesktopDevice() { - linkNewDesktopHandler.reset() - backstack.push(NavTarget.DesktopNotice) + override fun onUnlockDevice(type: LinkDeviceType) { + val callback = object : DeviceUnlockEntryPoint.Callback { + override fun onCancel() = Unit + override fun onUnlock() = when (type) { + LinkDeviceType.Mobile -> { + linkNewMobileHandler.reset() + linkNewMobileHandler.createAndStartNewHandler() + } + LinkDeviceType.Desktop -> { + linkNewDesktopHandler.reset() + backstack.push(NavTarget.DesktopNotice) + } + } + } + deviceUnlockEntryPoint.requestUnlock(callback) } } createNode(buildContext, listOf(callback)) } + is NavTarget.LockScreen -> { + deviceUnlockEntryPoint.createNode(this, buildContext) + } NavTarget.DesktopNotice -> { val callback = object : DesktopNoticeNode.Callback { override fun navigateBack() { @@ -250,6 +293,18 @@ class LinkNewDeviceFlowNode( } createNode(buildContext, listOf(callback)) } + is NavTarget.CodeConfirmation -> { + val callback = object : CodeConfirmationNode.Callback { + override fun onCancel() { + // Push error + backstack.push(NavTarget.Error(ErrorScreenType.Cancelled)) + } + } + val inputs = CodeConfirmationNode.Inputs( + code = navTarget.code, + ) + createNode(buildContext, listOf(inputs, callback)) + } is NavTarget.MobileShowQrCode -> { val callback = object : ShowQrCodeNode.Callback { override fun navigateBack() { @@ -281,8 +336,12 @@ class LinkNewDeviceFlowNode( } } - private fun navigateToBrowser(url: String) { - activity?.openUrlInChromeCustomTab(null, darkTheme, url) + private suspend fun navigateToBrowser(url: String) { + activity?.openUrlInChromeCustomTab( + session = null, + darkTheme = darkTheme, + url = sessionEnterpriseService.tweakMasUrl(url), + ) } @Composable @@ -295,5 +354,6 @@ class LinkNewDeviceFlowNode( } } BackstackView() + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockScreen) } } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt index 157d946eaa..39c9c6fef3 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt @@ -12,6 +12,7 @@ import dev.zacsweers.metro.SingleIn import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.linknewdevice.ErrorType import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep import io.element.android.libraries.matrix.api.logs.LoggerTags @@ -42,12 +43,15 @@ class LinkNewMobileHandler( val stepFlow: StateFlow get() = linkMobileStepFlow.asStateFlow() - fun createAndStartNewHandler() { + fun createAndStartNewHandler(forRotating: Boolean = false) { Timber.tag(loggerTag.value).d("createAndStartNewHandler()") currentJob?.cancel() handler = matrixClient.createLinkMobileHandler().getOrNull() handler?.let { h -> currentJob = sessionScope.launch { + if (!forRotating) { + linkMobileStepFlow.emit(LinkMobileStep.CreatingQrCode) + } h.linkMobileStep .onEach { linkMobileStepFlow.emit(it) @@ -65,4 +69,15 @@ class LinkNewMobileHandler( linkMobileStepFlow.emit(LinkMobileStep.Uninitialized) } } + + fun rotateQrCode() { + createAndStartNewHandler(forRotating = true) + } + + fun onTooManyRotation() { + reset() + sessionScope.launch { + linkMobileStepFlow.emit(LinkMobileStep.Error(ErrorType.Expired("Too many QR code rotations"))) + } + } } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationNode.kt new file mode 100644 index 0000000000..a8db4d2d75 --- /dev/null +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationNode.kt @@ -0,0 +1,47 @@ +/* + * 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.linknewdevice.impl.screens.confirmation + +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.NodeInputs +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +@AssistedInject +class CodeConfirmationNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext = buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onCancel() + } + + data class Inputs( + val code: String, + ) : NodeInputs + + private val callback: Callback = callback() + private val input = inputs() + + @Composable + override fun View(modifier: Modifier) { + CodeConfirmationView( + code = input.code, + onCancel = callback::onCancel, + ) + } +} diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationView.kt new file mode 100644 index 0000000000..d981574f86 --- /dev/null +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/confirmation/CodeConfirmationView.kt @@ -0,0 +1,134 @@ +/* + * 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.linknewdevice.impl.screens.confirmation + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +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.linknewdevice.impl.R +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.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun CodeConfirmationView( + code: String, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = onCancel) + FlowStepPage( + modifier = modifier, + iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()), + title = stringResource(R.string.screen_qr_code_login_device_code_title), + subTitle = stringResource(R.string.screen_qr_code_login_device_code_subtitle), + content = { Content(code = code) }, + buttons = { Buttons(onCancel = onCancel) } + ) +} + +@Composable +private fun Content(code: String) { + Column( + modifier = Modifier.padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Digits(code = code) + Spacer(modifier = Modifier.height(32.dp)) + WaitingForOtherDevice() + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun Digits(code: String) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + code.forEach { + Text( + modifier = Modifier + .padding(horizontal = 6.dp, vertical = 4.dp) + .clip(RoundedCornerShape(4.dp)) + .background(ElementTheme.colors.bgActionSecondaryPressed) + .padding(horizontal = 16.dp, vertical = 17.dp), + text = it.toString() + ) + } + } +} + +@Composable +private fun WaitingForOtherDevice() { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + CircularProgressIndicator( + modifier = Modifier + .size(20.dp) + .padding(2.dp), + strokeWidth = 2.dp, + ) + Text( + text = stringResource(R.string.screen_qr_code_login_verify_code_loading), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun Buttons( + onCancel: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_cancel), + onClick = onCancel, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun CodeConfirmationViewPreview() { + ElementPreview { + CodeConfirmationView( + code = "67", + onCancel = {}, + ) + } +} diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenType.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenType.kt index b92a19ef8a..ad8cc276c5 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenType.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenType.kt @@ -20,6 +20,9 @@ sealed interface ErrorScreenType : NodeInputs, Parcelable { @Parcelize data object Expired : ErrorScreenType + @Parcelize + data object OtherDeviceAlreadySignedIn : ErrorScreenType + @Parcelize data object Mismatch2Digits : ErrorScreenType diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt index 7fd699101b..5946eb9ab2 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt @@ -19,5 +19,6 @@ class ErrorScreenTypeProvider : PreviewParameterProvider { ErrorScreenType.InsecureChannelDetected, ErrorScreenType.SlidingSyncNotAvailable, ErrorScreenType.UnknownError, + ErrorScreenType.OtherDeviceAlreadySignedIn, ) } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt index 9f67e8bc17..7b830626f6 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt @@ -47,43 +47,54 @@ fun ErrorView( ) { val appName = LocalBuildMeta.current.applicationName BackHandler(onBack = onCancel) + val iconStyle = when (errorScreenType) { + ErrorScreenType.OtherDeviceAlreadySignedIn -> BigIcon.Style.SuccessSolid + else -> BigIcon.Style.AlertSolid + } FlowStepPage( modifier = modifier, - iconStyle = BigIcon.Style.AlertSolid, + iconStyle = iconStyle, title = titleText(errorScreenType, appName), subTitle = subtitleText(errorScreenType, appName), content = { Content(errorScreenType) }, buttons = { - Buttons( - onRetry = onRetry, - onCancel = onCancel, - ) + when (errorScreenType) { + ErrorScreenType.OtherDeviceAlreadySignedIn -> DoneButton( + onDone = onCancel, + ) + else -> Buttons( + onRetry = onRetry, + onCancel = onCancel, + ) + } }, ) } @Composable private fun titleText(errorScreenType: ErrorScreenType, appName: String) = when (errorScreenType) { - ErrorScreenType.Cancelled -> stringResource(R.string.screen_qr_code_login_error_cancelled_title) + ErrorScreenType.Cancelled -> stringResource(R.string.screen_link_new_device_error_request_cancelled_title) ErrorScreenType.Declined -> stringResource(R.string.screen_qr_code_login_error_declined_title) - ErrorScreenType.Expired -> stringResource(R.string.screen_qr_code_login_error_expired_title) - ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_qr_code_login_error_linking_not_suported_title) + ErrorScreenType.Expired -> stringResource(R.string.screen_link_new_device_error_request_timeout_title) + ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_link_new_device_error_not_supported_title) ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_title) ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_title) ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_title, appName) is ErrorScreenType.UnknownError -> stringResource(CommonStrings.common_something_went_wrong) + ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_title) } @Composable private fun subtitleText(errorScreenType: ErrorScreenType, appName: String) = when (errorScreenType) { - ErrorScreenType.Cancelled -> stringResource(R.string.screen_qr_code_login_error_cancelled_subtitle) + ErrorScreenType.Cancelled -> stringResource(R.string.screen_link_new_device_error_request_cancelled_subtitle) ErrorScreenType.Declined -> stringResource(R.string.screen_qr_code_login_error_declined_subtitle) - ErrorScreenType.Expired -> stringResource(R.string.screen_qr_code_login_error_expired_subtitle) - ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_qr_code_login_error_linking_not_suported_subtitle, appName) + ErrorScreenType.Expired -> stringResource(R.string.screen_link_new_device_error_request_timeout_subtitle) + ErrorScreenType.ProtocolNotSupported -> stringResource(R.string.screen_link_new_device_error_not_supported_subtitle) ErrorScreenType.Mismatch2Digits -> stringResource(id = R.string.screen_link_new_device_wrong_number_subtitle) ErrorScreenType.InsecureChannelDetected -> stringResource(id = R.string.screen_qr_code_login_connection_note_secure_state_description) ErrorScreenType.SlidingSyncNotAvailable -> stringResource(id = R.string.screen_qr_code_login_error_sliding_sync_not_supported_subtitle, appName) is ErrorScreenType.UnknownError -> stringResource(R.string.screen_qr_code_login_unknown_error_description) + ErrorScreenType.OtherDeviceAlreadySignedIn -> stringResource(R.string.screen_qr_code_login_error_device_already_signed_in_subtitle) } @Composable @@ -125,7 +136,18 @@ private fun Content(errorScreenType: ErrorScreenType) { } @Composable -private fun Buttons( +private fun DoneButton( + onDone: () -> Unit, +) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(CommonStrings.action_done), + onClick = onDone, + ) +} + +@Composable +private fun ColumnScope.Buttons( onRetry: () -> Unit, onCancel: () -> Unit, ) { diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeNode.kt index a884c3e97f..20bd50f488 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeNode.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeNode.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.di.SessionScope class ShowQrCodeNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + showQrCodePresenterFactory: ShowQrCodePresenter.Factory, ) : Node(buildContext, plugins = plugins) { class Inputs( val data: String, @@ -36,11 +37,15 @@ class ShowQrCodeNode( private val inputs: Inputs = inputs() private val callback: Callback = callback() + private val showQrCodePresenter: ShowQrCodePresenter = showQrCodePresenterFactory.create( + initialData = inputs.data, + ) @Composable override fun View(modifier: Modifier) { + val state = showQrCodePresenter.present() ShowQrCodeView( - data = inputs.data, + state = state, modifier = modifier, onBackClick = callback::navigateBack, ) diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenter.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenter.kt new file mode 100644 index 0000000000..21071a6831 --- /dev/null +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenter.kt @@ -0,0 +1,87 @@ +/* + * 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.linknewdevice.impl.screens.qrcode + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +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.linknewdevice.impl.LinkNewMobileHandler +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep +import io.element.android.libraries.matrix.api.logs.LoggerTags +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + +private val tag = LoggerTag("ShowQrCodePresenter", LoggerTags.linkNewDevice) + +@AssistedInject +class ShowQrCodePresenter( + @Assisted private val initialData: String, + private val linkNewMobileHandler: LinkNewMobileHandler, +) : Presenter { + @AssistedFactory + interface Factory { + fun create(initialData: String): ShowQrCodePresenter + } + + private var loadingJob: Job? = null + + @Composable + override fun present(): ShowQrCodeState { + var qrCodeRotationCounter by remember { mutableIntStateOf(MAX_QR_CODE_ROTATION) } + val state by produceState( + initialValue = ShowQrCodeState( + data = AsyncData.Success(initialData), + ) + ) { + linkNewMobileHandler.stepFlow.collect { step -> + when (step) { + is LinkMobileStep.QrReady -> { + loadingJob?.cancel() + value = ShowQrCodeState( + data = AsyncData.Success(step.data), + ) + } + is LinkMobileStep.QrRotating -> { + if (qrCodeRotationCounter-- > 0) { + Timber.tag(tag.value).d("Rotating QrCode") + linkNewMobileHandler.rotateQrCode() + // Ensure that outdated data is not rendered too long while rotating QR code + loadingJob = launch { + delay(1000) + value = ShowQrCodeState( + data = AsyncData.Loading(), + ) + } + } else { + Timber.tag(tag.value).w("Max QR code rotation reached, not rotating anymore") + linkNewMobileHandler.onTooManyRotation() + } + } + else -> Unit + } + } + } + + return state + } + + companion object { + const val MAX_QR_CODE_ROTATION = 10 + } +} diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeState.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeState.kt new file mode 100644 index 0000000000..e69dde8264 --- /dev/null +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeState.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.features.linknewdevice.impl.screens.qrcode + +import io.element.android.libraries.architecture.AsyncData + +data class ShowQrCodeState( + val data: AsyncData, +) diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt new file mode 100644 index 0000000000..e6d33c2544 --- /dev/null +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt @@ -0,0 +1,27 @@ +/* + * 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.linknewdevice.impl.screens.qrcode + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData + +class ShowQrCodeStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aShowQrCodeState(), + aShowQrCodeState( + data = AsyncData.Loading(), + ), + ) +} + +internal fun aShowQrCodeState( + data: AsyncData = AsyncData.Success("DATA"), +) = ShowQrCodeState( + data = data, +) diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt index 501415f621..f2cd07f4a5 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt @@ -9,6 +9,12 @@ package io.element.android.features.linknewdevice.impl.screens.qrcode +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -21,6 +27,7 @@ import androidx.compose.ui.Alignment 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.linknewdevice.impl.R @@ -30,6 +37,7 @@ 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.LocalBuildMeta +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.utils.annotatedTextWithBold import io.element.android.libraries.qrcode.QrCodeImage import kotlinx.collections.immutable.persistentListOf @@ -38,9 +46,10 @@ import kotlinx.collections.immutable.persistentListOf * QrCode display screen: * https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=2027-23617 */ +@OptIn(ExperimentalAnimationApi::class) @Composable fun ShowQrCodeView( - data: String, + state: ShowQrCodeState, onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -55,11 +64,17 @@ fun ShowQrCodeView( Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - QrCodeImage( - data = data, - modifier = Modifier - .size(220.dp) - ) + AnimatedContent( + modifier = Modifier.size(220.dp), + targetState = state.data.dataOrNull(), + transitionSpec = { + fadeIn().togetherWith(fadeOut()) + } + ) { data -> + QrCodeOrLoading( + data = data, + ) + } Spacer(modifier = Modifier.height(32.dp)) NumberedListOrganism( modifier = Modifier.fillMaxSize(), @@ -79,11 +94,33 @@ fun ShowQrCodeView( } } +@Composable +private fun QrCodeOrLoading( + data: String?, + modifier: Modifier = Modifier, +) { + if (data == null) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } else { + QrCodeImage( + modifier = modifier, + data = data, + ) + } +} + @PreviewsDayNight @Composable -internal fun ShowQrCodeViewPreview() = ElementPreview { +internal fun ShowQrCodeViewPreview( + @PreviewParameter(ShowQrCodeStateProvider::class) state: ShowQrCodeState, +) = ElementPreview { ShowQrCodeView( - data = "DATA", + state = state, onBackClick = { }, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkDeviceType.kt similarity index 60% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkDeviceType.kt index 529dd4eb4e..6b26fe5e79 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkDeviceType.kt @@ -5,8 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.certificates +package io.element.android.features.linknewdevice.impl.screens.root -interface UserCertificatesProvider { - fun provides(): List +enum class LinkDeviceType { + Mobile, + Desktop, } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootEvent.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootEvent.kt index 8ce6af90b0..97047fea9d 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootEvent.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootEvent.kt @@ -8,6 +8,5 @@ package io.element.android.features.linknewdevice.impl.screens.root sealed interface LinkNewDeviceRootEvent { - data object LinkMobileDevice : LinkNewDeviceRootEvent data object CloseDialog : LinkNewDeviceRootEvent } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootNode.kt index f43ffa64df..dc74f1287f 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootNode.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootNode.kt @@ -27,7 +27,7 @@ class LinkNewDeviceRootNode( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun onDone() - fun linkDesktopDevice() + fun onUnlockDevice(type: LinkDeviceType) } private val callback: Callback = callback() @@ -39,7 +39,7 @@ class LinkNewDeviceRootNode( state = state, modifier = modifier, onBackClick = callback::onDone, - onLinkDesktopDeviceClick = callback::linkDesktopDevice, + onUnlockDevice = callback::onUnlockDevice, ) } } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenter.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenter.kt index a17ed88fd3..af3ede0c27 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenter.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenter.kt @@ -50,7 +50,13 @@ class LinkNewDeviceRootPresenter( LaunchedEffect(step) { when (val finalStep = step) { is LinkMobileStep.Uninitialized -> { - qrCodeData = AsyncData.Uninitialized + // Ignore this step when loading QrCode + if (!qrCodeData.isLoading()) { + qrCodeData = AsyncData.Uninitialized + } + } + is LinkMobileStep.CreatingQrCode -> { + qrCodeData = AsyncData.Loading() } is LinkMobileStep.QrReady -> { qrCodeData = AsyncData.Success(Unit) @@ -64,12 +70,6 @@ class LinkNewDeviceRootPresenter( fun handleEvent(event: LinkNewDeviceRootEvent) { when (event) { - LinkNewDeviceRootEvent.LinkMobileDevice -> coroutineScope.launch { - qrCodeData = AsyncData.Loading() - // Wait for the QrCode to be ready - linkNewMobileHandler.reset() - linkNewMobileHandler.createAndStartNewHandler() - } LinkNewDeviceRootEvent.CloseDialog -> coroutineScope.launch { linkNewMobileHandler.reset() } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt index d9249d3e89..5778abfb2c 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt @@ -41,7 +41,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LinkNewDeviceRootView( state: LinkNewDeviceRootState, onBackClick: () -> Unit, - onLinkDesktopDeviceClick: () -> Unit, + onUnlockDevice: (type: LinkDeviceType) -> Unit, modifier: Modifier = Modifier, ) { val (title, subtitle, iconStyle) = if (state.isSupported.dataOrNull() == false) { @@ -57,6 +57,7 @@ fun LinkNewDeviceRootView( BigIcon.Style.Default(CompoundIcons.Devices()) ) } + FlowStepPage( onBackClick = onBackClick, title = title, @@ -83,40 +84,37 @@ fun LinkNewDeviceRootView( } is AsyncData.Success -> { if (state.isSupported.data) { - when (state.qrCodeData) { - AsyncData.Uninitialized, - is AsyncData.Failure -> { - Button( - onClick = { state.eventSink(LinkNewDeviceRootEvent.LinkMobileDevice) }, - text = stringResource(id = R.string.screen_link_new_device_root_mobile_device), - modifier = Modifier.fillMaxWidth(), - leadingIcon = IconSource.Vector(CompoundIcons.Mobile()), - ) - Button( - onClick = onLinkDesktopDeviceClick, - text = stringResource(id = R.string.screen_link_new_device_root_desktop_computer), - modifier = Modifier.fillMaxWidth(), - leadingIcon = IconSource.Vector(CompoundIcons.Computer()), - ) - } - is AsyncData.Loading, - is AsyncData.Success -> { - Button( - onClick = { state.eventSink(LinkNewDeviceRootEvent.LinkMobileDevice) }, - text = stringResource(id = R.string.screen_link_new_device_root_loading_qr_code), - showProgress = true, - enabled = false, - modifier = Modifier.fillMaxWidth(), - ) - Button( - onClick = onLinkDesktopDeviceClick, - text = stringResource(id = R.string.screen_link_new_device_root_desktop_computer), - modifier = Modifier.fillMaxWidth(), - enabled = false, - leadingIcon = IconSource.Vector(CompoundIcons.Computer()), - ) - } - } + val canClick = state.qrCodeData is AsyncData.Uninitialized + val isLoading = state.qrCodeData is AsyncData.Loading || state.qrCodeData is AsyncData.Success + Button( + onClick = { + if (canClick) { + onUnlockDevice(LinkDeviceType.Mobile) + } + }, + text = stringResource( + id = if (isLoading) { + R.string.screen_link_new_device_root_loading_qr_code + } else { + R.string.screen_link_new_device_root_mobile_device + } + ), + showProgress = isLoading, + enabled = !isLoading, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(CompoundIcons.Mobile()), + ) + Button( + onClick = { + if (canClick) { + onUnlockDevice(LinkDeviceType.Desktop) + } + }, + text = stringResource(id = R.string.screen_link_new_device_root_desktop_computer), + enabled = !isLoading, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(CompoundIcons.Computer()), + ) } else { Button( onClick = onBackClick, @@ -147,6 +145,6 @@ internal fun LinkNewDeviceRootViewPreview( LinkNewDeviceRootView( state = state, onBackClick = { }, - onLinkDesktopDeviceClick = { }, + onUnlockDevice = { }, ) } diff --git a/features/linknewdevice/impl/src/main/res/values-be/translations.xml b/features/linknewdevice/impl/src/main/res/values-be/translations.xml index 16372fa6e4..378a405c95 100644 --- a/features/linknewdevice/impl/src/main/res/values-be/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-be/translations.xml @@ -17,6 +17,8 @@ "Калі вы сутыкнуліся з той жа праблемай, паспрабуйце іншую сетку Wi-Fi або скарыстайцеся мабільнымі дадзенымі замест Wi-Fi." "Калі гэта не дапамагло, увайдзіце ўручную" "Злучэнне небяспечнае" + "Вам будзе прапанавана ўвесці дзве лічбы, паказаныя на гэтай прыладзе." + "Увядзіце наступны нумар на іншай прыладзе." "Уваход быў адменены на іншай прыладзе." "Запыт на ўваход скасаваны" "Уваход на іншай прыладзе быў адхілены." @@ -35,4 +37,5 @@ "Каб працягнуць, вам неабходна дазволіць %1$s выкарыстоўваць камеру вашай прылады." "Дазвольце доступ да камеры для сканіравання QR-кода" "Адбылася нечаканая памылка. Калі ласка, паспрабуйце яшчэ раз." + "У чаканні іншай прылады" diff --git a/features/linknewdevice/impl/src/main/res/values-ca/translations.xml b/features/linknewdevice/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..d78744ff85 --- /dev/null +++ b/features/linknewdevice/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,41 @@ + + + "Escaneja el QR" + "Escaneja el codi QR amb aquest dispositiu" + "Preparat per escanejar" + "El proveïdor del teu compte no admet %1$s." + "%1$s no és compatible" + "Codi QR no compatible" + "L\'inici de sessió s\'ha cancel·lat a l\'altre dispositiu." + "Sol·licitud d\'inici de sessió cancel·lada" + "Inici de sessió ha caducat. Torna-ho a provar." + "L\'inici de sessió no s\'ha completat a temps" + "Selecciona %1$s" + "No s\'ha pogut establir una connexió segura amb el dispositiu nou. Els dispositius existents continuen sent segurs, no te n\'has de preocupar." + "I ara què?" + "Prova de tornar a iniciar sessió mitjançant un codi QR si es tracta d\'un problema de xarxa." + "Si es repeteix el mateix problema, prova una xarxa wifi diferent o utilitza les dades mòbils en lloc del wifi." + "Si no funciona, inicia sessió manualment" + "Connexió no segura" + "Se\'t demanarà que introdueixis els dos dígits mostrats en aquest dispositiu." + "Introdueix el número següent a l\'altre dispositiu" + "L\'inici de sessió s\'ha cancel·lat a l\'altre dispositiu." + "Sol·licitud d\'inici de sessió cancel·lada" + "L\'inici de sessió s\'ha rebutjat a l\'altre dispositiu." + "Inici de sessió rebutjat" + "Inici de sessió ha caducat. Torna-ho a provar." + "L\'inici de sessió no s\'ha completat a temps" + "El teu altre dispositiu no admet l\'inici de sessió a %s amb codis QR. + +Prova d\'iniciar la sessió manualment o escaneja el QR amb un altre dispositiu." + "Codi QR no compatible" + "El proveïdor del teu compte no admet %1$s." + "%1$s no és compatible" + "Utilitza el codi QR que es mostra a l\'altre dispositiu." + "Torna-ho a intentar" + "Codi QR incorrecte" + "Per continuar, has donar permís a %1$s per poder utilitzar la càmera del dispositiu." + "Permet l\'accés a la càmera per poder escanejar el codi QR" + "S\'ha produït un error inesperat. Torna-ho a provar." + "Esperant el teu altre dispositiu" + diff --git a/features/linknewdevice/impl/src/main/res/values-cs/translations.xml b/features/linknewdevice/impl/src/main/res/values-cs/translations.xml index 4b8f230d55..e0150668a3 100644 --- a/features/linknewdevice/impl/src/main/res/values-cs/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-cs/translations.xml @@ -34,6 +34,8 @@ "Pokud narazíte na stejný problém, zkuste jinou síť wifi nebo použijte mobilní data místo wifi" "Pokud to nefunguje, přihlaste se ručně" "Připojení není zabezpečené" + "Budete požádáni o zadání dvou níže uvedených číslic." + "Zadejte níže uvedené číslo na svém dalším zařízení" "Přihlášení bylo na druhém zařízení zrušeno." "Žádost o přihlášení zrušena" "Přihlášení bylo na druhém zařízení odmítnuto." @@ -54,4 +56,5 @@ Zkuste se přihlásit ručně nebo naskenujte QR kód pomocí jiného zařízen "Abyste mohli pokračovat, musíte aplikaci %1$s udělit povolení k použití kamery vašeho zařízení." "Povolte přístup k fotoaparátu a naskenujte QR kód" "Vyskytla se neočekávaná chyba. Prosím zkuste to znovu." + "Čekání na vaše další zařízení" diff --git a/features/linknewdevice/impl/src/main/res/values-cy/translations.xml b/features/linknewdevice/impl/src/main/res/values-cy/translations.xml index b26aed52ef..6b1cb7781f 100644 --- a/features/linknewdevice/impl/src/main/res/values-cy/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-cy/translations.xml @@ -17,6 +17,8 @@ "Os ydych chi\'n dod ar draws yr un broblem, rhowch gynnig ar rwydwaith wifi gwahanol neu defnyddiwch eich data symudol yn lle wifi" "Os nad yw hynny\'n gweithio, mewngofnodwch â llaw" "Nid yw\'r cysylltiad yn ddiogel" + "Bydd gofyn i chi nodi\'r ddau ddigid sy\'n cael eu dangos ar y ddyfais hon." + "Rhowch y rhif isod ar eich dyfais arall" "Cafodd y mewngofnodi ei ddiddymu ar y ddyfais arall." "Cais mewngofnodi wedi\'i ddiddymu" "Cafodd y mewngofnodi ar y ddyfais arall ei wrthod." @@ -35,4 +37,5 @@ Ceisiwch fewngofnodi â llaw, neu sganiwch y cod QR gyda dyfais arall." "Mae angen i chi roi caniatâd i %1$s ddefnyddio camera eich dyfais er mwyn parhau." "Caniatáu mynediad camera i sganio\'r cod QR" "Digwyddodd gwall annisgwyl. Ceisiwch eto." + "Yn aros am eich dyfais arall" diff --git a/features/linknewdevice/impl/src/main/res/values-da/translations.xml b/features/linknewdevice/impl/src/main/res/values-da/translations.xml index 5bc9f04fb9..45f510e90b 100644 --- a/features/linknewdevice/impl/src/main/res/values-da/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-da/translations.xml @@ -34,6 +34,8 @@ "Hvis du støder på det samme problem, kan du prøve et andet wifi-netværk eller bruge dine mobildata i stedet for wifi" "Hvis det ikke virker, skal du logge ind manuelt" "Forbindelsen er ikke sikker" + "Du bliver bedt om at indtaste de to cifre, der vises på denne enhed." + "Indtast nummeret herunder på din anden enhed" "Login blev annulleret på den anden enhed." "Anmodning om login annulleret" "Login blev afvist på den anden enhed." @@ -54,4 +56,5 @@ Prøv at logge ind manuelt, eller scan QR-koden med en anden enhed." "Du skal give tilladelse til at %1$s kan benytte enhedens kamera, for at fortsætte." "Tillad kameraadgang for at scanne QR-koden" "Der opstod en uventet fejl. Prøv venligst igen." + "Venter på din anden enhed" diff --git a/features/linknewdevice/impl/src/main/res/values-de/translations.xml b/features/linknewdevice/impl/src/main/res/values-de/translations.xml index b8ad8b80ef..773878c736 100644 --- a/features/linknewdevice/impl/src/main/res/values-de/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-de/translations.xml @@ -34,6 +34,8 @@ "Wenn das Problem bestehen bleibt, versuche es mit einem anderen WLAN-Netzwerk oder verwende deine mobilen Daten statt WLAN." "Wenn das nicht funktioniert, melde dich manuell an" "Die Verbindung ist nicht sicher" + "Du wirst aufgefordert, die beiden unten abgebildeten Ziffern einzugeben." + "Trage die unten angezeigte Zahl auf einem anderen Device ein" "Die Anmeldung wurde auf dem anderen Gerät abgebrochen." "Anmeldeanfrage abgebrochen" "Die Anmeldung auf dem anderen Gerät wurde abgelehnt." @@ -54,4 +56,5 @@ Versuche, dich manuell anzumelden, oder scanne den QR-Code mit einem anderen Ger "Du musst %1$s die Berechtigung erteilen, die Kamera deines Geräts zu verwenden, um fortzufahren." "Erlaube Zugriff auf die Kamera zum Scannen des QR-Codes" "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut." + "Warten auf dein anderes Gerät" diff --git a/features/linknewdevice/impl/src/main/res/values-el/translations.xml b/features/linknewdevice/impl/src/main/res/values-el/translations.xml index 26c917075b..6c0e77da40 100644 --- a/features/linknewdevice/impl/src/main/res/values-el/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-el/translations.xml @@ -34,6 +34,8 @@ "Εάν αντιμετωπίσεις το ίδιο πρόβλημα, δοκίμασε ένα διαφορετικό δίκτυο wifi ή χρησιμοποίησε τα δεδομένα του κινητού σου αντί για wifi" "Εάν δεν λειτουργήσει, συνδέσου χειροκίνητα" "Η σύνδεση δεν είναι ασφαλής" + "Θα σου ζητηθεί να εισάγεις τα δύο ψηφία που εμφανίζονται σε αυτήν τη συσκευή." + "Εισήγαγε τον παρακάτω αριθμό στην άλλη συσκευή σου" "Η σύνδεση ακυρώθηκε στην άλλη συσκευή." "Το αίτημα σύνδεσης ακυρώθηκε" "Η σύνδεση απορρίφθηκε στην άλλη συσκευή." @@ -54,4 +56,5 @@ "Πρέπει να δώσεις άδεια για %1$s για να χρησιμοποιήσεις την κάμερα της συσκευής σου και να συνεχίσεις." "Επέτρεψε την πρόσβαση της κάμερας για σάρωση του κωδικού QR" "Παρουσιάστηκε ένα απροσδόκητο σφάλμα. Παρακαλώ προσπάθησε ξανά." + "Αναμονή για την άλλη σου συσκευή" diff --git a/features/linknewdevice/impl/src/main/res/values-es/translations.xml b/features/linknewdevice/impl/src/main/res/values-es/translations.xml index 032813a2c4..c33dc3ad88 100644 --- a/features/linknewdevice/impl/src/main/res/values-es/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-es/translations.xml @@ -17,6 +17,8 @@ "Si te encuentras con el mismo problema, prueba con una red wifi diferente o usa tus datos móviles en lugar de wifi" "Si eso no funciona, inicia sesión manualmente" "La conexión no es segura" + "Se te pedirá que introduzcas los dos dígitos mostrados en este dispositivo." + "Introduce el número que aparece a continuación en tu otro dispositivo" "El inicio de sesión se canceló en el otro dispositivo." "Solicitud de inicio de sesión cancelada" "El inicio de sesión se rechazó en el otro dispositivo." @@ -35,4 +37,5 @@ Intenta iniciar sesión manualmente o escanea el código QR con otro dispositivo "Tienes que dar permiso a %1$s para que utilice la cámara de tu dispositivo y así poder continuar." "Permite el acceso a la cámara para escanear el código QR" "Se ha producido un error inesperado. Vuelve a intentarlo." + "A la espera de tu otro dispositivo" diff --git a/features/linknewdevice/impl/src/main/res/values-et/translations.xml b/features/linknewdevice/impl/src/main/res/values-et/translations.xml index 6aa1398e0a..10f8af5e11 100644 --- a/features/linknewdevice/impl/src/main/res/values-et/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-et/translations.xml @@ -34,6 +34,8 @@ "Kui sama probleem kordub, siis kasuta mõnda muud WiFi- või mobiilset andmedsideühendust" "Kui see ka ei aita, siis logi sisse käsitsi" "Ühendus pole turvaline" + "Sul palutakse sisestada kaks selles seadmes kuvatud numbrit." + "Sisesta see number oma teises seadmes" "Sisselogimine katkestati teises seadmes." "Sisselogimispäring on tühistatud" "Sisselogimisest on teises seadmes keeldutud." @@ -54,4 +56,5 @@ Proovi käsitsi sisselogimist või skaneeri QR-koodi mõne muu seadmega.""Jätkamiseks pead lubama, et %1$s saab kasutada sinu nutiseadme kaamerat" "QR-koodi lugemiseks luba kaamerat kasutada" "Tekkis ootamatu viga. Palun proovi uuesti." + "Ootame sinu teise seadme järgi" diff --git a/features/linknewdevice/impl/src/main/res/values-eu/translations.xml b/features/linknewdevice/impl/src/main/res/values-eu/translations.xml index 06cc0fd857..8680ad94c7 100644 --- a/features/linknewdevice/impl/src/main/res/values-eu/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-eu/translations.xml @@ -16,6 +16,8 @@ "Saiatu berriro QR kodearekin saioa hasten sare-arazo bat izan bada" "Horrek ez badu funtzionatzen, hasi saioa eskuz" "Konexioa ez da segurua" + "Gailu honetan agertzen diren bi digituak sartzeko eskatuko zaizu." + "Sartu beheko zenbakia beste gailuan" "Saioa hasteko eskaera bertan behera utzi da beste gailuan" "Saioa hasteko eskaera bertan behera utzi da" "Saioa hasteari uko egin zaio beste dispositiboan." @@ -33,4 +35,5 @@ Saiatu saioa eskuz hasten, edo eskaneatu QR kodea beste gailu batean." "QR kode okerra" "Baimendu kameraren sarbidea QR kodea eskaneatzeko" "Ustekabeko errore bat gertatu da. Saiatu berriro." + "Beste gailuaren zain" diff --git a/features/linknewdevice/impl/src/main/res/values-fa/translations.xml b/features/linknewdevice/impl/src/main/res/values-fa/translations.xml index 804fa653ad..07c329ef6a 100644 --- a/features/linknewdevice/impl/src/main/res/values-fa/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fa/translations.xml @@ -15,6 +15,8 @@ "اکنون چه؟" "ورود دستی در صورت کار نکردنش" "اتّصال ناامن" + "از شما خواسته خواهد شد که دو رقم نشان داده روی این افزاره را وارد کنید." + "شمارهٔ زیر را روی افزارهٔ دیگرتان وارد کنید" "ورود روی افزارهٔ دیگر لغو شد." "درخواست ورد لغو شد" "ورود به دست افزارهٔ دیگر رد شد." @@ -33,4 +35,5 @@ "برای ادامه باید اجازهٔ استفادهٔ %1$s از دوربین افزاره‌تان را بدهید." "اجازهٔ دسترسی دوربین برای پویش کد پاس" "خطایی غیرمنتظره رخ داد. لطفاً دوباره تلاش کنید." + "منتظر افزارهٔ دیگرتان" diff --git a/features/linknewdevice/impl/src/main/res/values-fi/translations.xml b/features/linknewdevice/impl/src/main/res/values-fi/translations.xml index f8e999f886..0ba5a30e58 100644 --- a/features/linknewdevice/impl/src/main/res/values-fi/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fi/translations.xml @@ -34,6 +34,8 @@ "Jos kohtaat saman ongelman, kokeile toista wifi-verkkoa tai käytä mobiilidataa wifi-yhteyden sijaan" "Jos tämä ei auta, kirjaudu sisään manuaalisesti" "Yhteys ei ole turvallinen" + "Sinua pyydetään antamaan tässä laitteessa näkyvät kaksi numeroa." + "Kirjoita alla oleva numero toisella laitteellasi" "Kirjautuminen peruutettiin toisella laitteella." "Kirjautumispyyntö peruutettu" "Kirjautuminen hylättiin toisella laitteella." @@ -54,4 +56,5 @@ Yritä kirjautua sisään manuaalisesti tai skannaa QR-koodi toisella laitteella "Jatkaaksesi sinun on annettava lupa %1$s -sovellukselle käyttää laitteesi kameraa." "Salli lupa kameraan QR-koodin skannaamiseksi" "Tapahtui odottamaton virhe. Yritä uudelleen." + "Odotetaan toista laitettasi" diff --git a/features/linknewdevice/impl/src/main/res/values-fr/translations.xml b/features/linknewdevice/impl/src/main/res/values-fr/translations.xml index 0c91dca7a1..12a770af17 100644 --- a/features/linknewdevice/impl/src/main/res/values-fr/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-fr/translations.xml @@ -34,6 +34,8 @@ "Si vous rencontrez le même problème, essayez un autre réseau wifi ou utilisez vos données mobiles au lieu du wifi" "Si cela ne fonctionne pas, connectez-vous manuellement" "La connexion n’est pas sécurisée" + "Il vous sera demandé de saisir les deux chiffres affichés sur cet appareil." + "Saisissez le nombre ci-dessous sur votre autre appareil" "La connexion a été annulée sur l’autre appareil." "Demande de connexion annulée" "La connexion a été refusée sur l’autre appareil." @@ -52,4 +54,5 @@ "Vous devez autoriser %1$s à utiliser la camera de votre appareil pour continuer." "Autoriser l’usage de la caméra pour scanner le code QR" "Une erreur inattendue s’est produite. Veuillez réessayer." + "En attente de votre autre session" diff --git a/features/linknewdevice/impl/src/main/res/values-hr/translations.xml b/features/linknewdevice/impl/src/main/res/values-hr/translations.xml index 20c194ef93..8561e63ad3 100644 --- a/features/linknewdevice/impl/src/main/res/values-hr/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-hr/translations.xml @@ -34,6 +34,8 @@ "Ako se problem ponovi, pokušajte s drugom Wi-Fi mrežom ili mobilnim podatcima umjesto Wi-Fi-ja." "Ako to ne uspije, prijavite se ručno" "Veza nije sigurna" + "Od vas će se zatražiti da unesete dvije znamenke prikazane na ovom uređaju." + "Unesite ispod navedeni broj u svoj drugi uređaj" "Prijava je otkazana na drugom uređaju." "Zahtjev za prijavu je otkazan" "Prijava je odbijena na drugom uređaju." @@ -54,4 +56,5 @@ Pokušajte se prijaviti ručno ili skenirajte QR kod drugim uređajem." "Za nastavak morate dati dopuštenje za %1$s da biste se mogli služiti kamerom svog uređaja." "Dopustite pristup kameri kako biste mogli skenirati QR kod" "Došlo je do neočekivane pogreške. Pokušajte ponovno." + "Čekanje na vaš drugi uređaj" diff --git a/features/linknewdevice/impl/src/main/res/values-hu/translations.xml b/features/linknewdevice/impl/src/main/res/values-hu/translations.xml index 51fe30bbd8..8cefed94cc 100644 --- a/features/linknewdevice/impl/src/main/res/values-hu/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-hu/translations.xml @@ -34,6 +34,8 @@ "Ha ugyanezzel a problémával találkozik, próbálkozzon másik Wi-Fi-hálózattal, vagy a Wi-Fi helyett használja a mobil-adatkapcsolatát" "Ha ez nem működik, jelentkezzen be kézileg" "A kapcsolat nem biztonságos" + "A rendszer kérni fogja, hogy adja meg az alábbi két számjegyet az eszközén." + "Adja meg az alábbi számot a másik eszközén" "A bejelentkezést megszakították a másik eszközön." "Bejelentkezési kérés törölve" "A bejelentkezést elutasították a másik eszközön." @@ -54,4 +56,5 @@ Próbáljon meg kézileg bejelentkezni, vagy olvassa be a QR-kódot egy másik e "A folytatáshoz engedélyeznie kell, hogy az %1$s használhassa az eszköz kameráját." "Engedélyezze a kamera elérését a QR-kód beolvasásához" "Váratlan hiba történt. Próbálja meg újra." + "Várakozás a másik eszközre" diff --git a/features/linknewdevice/impl/src/main/res/values-in/translations.xml b/features/linknewdevice/impl/src/main/res/values-in/translations.xml index 20badba9ba..5508993090 100644 --- a/features/linknewdevice/impl/src/main/res/values-in/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-in/translations.xml @@ -17,6 +17,8 @@ "Jika Anda mengalami masalah yang sama, coba jaringan Wi-Fi yang berbeda atau gunakan data seluler Anda daripada Wi-Fi" "Jika tidak berhasil, masuk secara manual" "Koneksi tidak aman" + "Anda akan diminta untuk memasukkan dua digit yang ditunjukkan di perangkat ini." + "Masukkan nomor bawah di perangkat Anda yang lain" "Proses masuk dibatalkan di perangkat lain." "Permintaan masuk dibatalkan" "Proses masuk ditolak di perangkat lain." @@ -35,4 +37,5 @@ Coba masuk secara manual, atau pindai kode QR dengan perangkat lain." "Anda perlu memberikan izin ke %1$s untuk menggunakan kamera perangkat Anda untuk melanjutkan." "Izinkan akses kamera untuk memindai kode QR" "Terjadi kesalahan tak terduga. Silakan coba lagi." + "Menunggu perangkat Anda yang lain" 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 9a6476823a..6a32c70fe4 100644 --- a/features/linknewdevice/impl/src/main/res/values-it/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-it/translations.xml @@ -34,6 +34,8 @@ "Se riscontri lo stesso problema, prova con un altra rete wifi o usa i dati mobili al posto del wifi." "Se il problema persiste, accedi manualmente" "La connessione non è sicura" + "Ti verrà chiesto di inserire le due cifre mostrate su questo dispositivo." + "Inserisci il numero qui sotto sull\'altro dispositivo" "L\'accesso è stato annullato sull\'altro dispositivo." "Richiesta di accesso annullata" "L\'accesso è stato rifiutato sull\'altro dispositivo." @@ -54,4 +56,5 @@ Prova ad accedere manualmente o scansiona il codice QR con un altro dispositivo. "Per continuare, è necessario fornire l\'autorizzazione a %1$s per utilizzare la fotocamera del dispositivo." "Consenti l\'accesso alla fotocamera per la scansione del codice QR" "Si è verificato un errore inatteso. Riprova." + "In attesa dell\'altro dispositivo" diff --git a/features/linknewdevice/impl/src/main/res/values-ja/translations.xml b/features/linknewdevice/impl/src/main/res/values-ja/translations.xml index 6cfd5baf84..c4f09dcd0e 100644 --- a/features/linknewdevice/impl/src/main/res/values-ja/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ja/translations.xml @@ -18,7 +18,7 @@ "サインインが無効です。もう一度試してください。" "サインインが時間内に完了しませんでした" "%1$s を他の端末で開いてください" - "%1$s を選択してください" + "%1$s を選択" "\"QRコードでサインイン\"" "表示されているQRコードを一方の端末で読み取ってください" "%1$s を他の端末で開いてください" @@ -34,6 +34,8 @@ "同様の問題が発生する場合は、異なるWi-Fiやモバイルデータ通信を試してください" "問題が解決しない場合は、手動でサインインしてください" "接続が安全ではありません" + "この端末に表示される2つの数字の入力を要求されます" + "もう一方に表示される数字を入力してください" "もう一方の端末がサインインをキャンセルしました" "サインインのリクエストがキャンセルされました" "もう一方の端末でサインインを拒否されました" @@ -54,4 +56,5 @@ "続行するには、%1$s にカメラの使用を許可する必要があります。" "QRコードを読み取るため、カメラへのアクセスを許可" "予期せぬ問題が発生しました。もう一度試してください。" + "一方の端末を待機しています" diff --git a/features/linknewdevice/impl/src/main/res/values-ko/translations.xml b/features/linknewdevice/impl/src/main/res/values-ko/translations.xml index 3b31c8fdc2..1d69f58ca5 100644 --- a/features/linknewdevice/impl/src/main/res/values-ko/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ko/translations.xml @@ -34,6 +34,8 @@ "동일한 문제를 겪으신 경우 다른 Wi-Fi 네트워크를 사용해 보거나 Wi-Fi 대신 모바일 데이터를 사용해 보세요." "만약 작동하지 않는 경우, 수동으로 로그인하세요." "연결이 안전하지 않습니다" + "이 장치에 표시된 두 자리 숫자를 입력하라는 메시지가 표시됩니다." + "다른 device 에 아래 번호를 입력하세요" "다른 기기에서 로그인이 취소되었습니다." "로그인 요청이 취소되었습니다" "다른 기기에서 로그인이 거부되었습니다." @@ -54,4 +56,5 @@ "계속하려면 %1$s 가 기기의 카메라를 사용할 수 있도록 권한을 부여해야 합니다." "카메라 액세스를 허용하여 QR 코드를 스캔하세요" "예기치 않은 오류가 발생했습니다. 다시 시도해 주세요." + "다른 기기를 기다리고 있습니다" diff --git a/features/linknewdevice/impl/src/main/res/values-nb/translations.xml b/features/linknewdevice/impl/src/main/res/values-nb/translations.xml index 6b8541b25b..4e8844ee2f 100644 --- a/features/linknewdevice/impl/src/main/res/values-nb/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-nb/translations.xml @@ -34,6 +34,8 @@ "Hvis du støter på det samme problemet, kan du prøve et annet wifi-nettverk eller bruke mobildata i stedet for wifi" "Hvis det ikke fungerer, kan du logge på manuelt" "Forbindelsen er ikke sikker" + "Du blir bedt om å skrive inn de to sifrene som vises på denne enheten." + "Skriv inn nummeret nedenfor på den andre enheten" "Påloggingen ble kansellert på den andre enheten." "Påloggingsforespørsel kansellert" "Påloggingen ble avvist på den andre enheten." @@ -54,4 +56,5 @@ Prøv å logge på manuelt, eller skann QR-koden med en annen enhet." "Du må gi tillatelse til at %1$s kan bruke enhetens kamera for å fortsette." "Tillat kameratilgang for å skanne QR-koden" "Det oppstod en uventet feil. Prøv igjen." + "Venter på den andre enheten din" diff --git a/features/linknewdevice/impl/src/main/res/values-nl/translations.xml b/features/linknewdevice/impl/src/main/res/values-nl/translations.xml index 407a470e48..6dbc2c18c2 100644 --- a/features/linknewdevice/impl/src/main/res/values-nl/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-nl/translations.xml @@ -17,6 +17,8 @@ "Als je hetzelfde probleem ondervindt, probeer dan een ander wifi-netwerk of gebruik je mobiele data in plaats van wifi." "Als dat niet werkt, log dan handmatig in" "Verbinding niet veilig" + "Daar word je gevraagd om de twee cijfers in te voeren die op dit apparaat worden weergegeven." + "Voer het onderstaande nummer in op je andere apparaat" "De aanmelding is geannuleerd op het andere apparaat." "Login verzoek geannuleerd" "De aanmelding is geweigerd op het andere apparaat." @@ -35,4 +37,5 @@ Probeer handmatig in te loggen, of scan de QR code met een ander apparaat.""Je moet %1$s toestemming geven om de camera van je apparaat te gebruiken om verder te gaan." "Cameratoegang toestaan om de QR-code te scannen" "Er is een onverwachte fout opgetreden. Probeer het opnieuw." + "Aan het wachten op je andere apparaat" diff --git a/features/linknewdevice/impl/src/main/res/values-pl/translations.xml b/features/linknewdevice/impl/src/main/res/values-pl/translations.xml index 4db42a2a49..ced8955d1d 100644 --- a/features/linknewdevice/impl/src/main/res/values-pl/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-pl/translations.xml @@ -1,26 +1,47 @@ "Skanuj kod QR" + "Otwórz %1$s na laptopie lub komputerze stacjonarnym" "Zeskanuj kod QR za pomocą tego urządzenia" "Gotowy do skanowania" + "Otwórz %1$s na komputerze stacjonarnym, aby uzyskać kod QR" + "Liczby nie pasują do siebie" + "Wprowadź 2-cyfrowy kod" + "Pozwoli to sprawdzić, czy połączenie z drugim urządzeniem jest bezpieczne." + "Wprowadź numer wyświetlany na drugim urządzeniu" "Twój dostawca konta nie obsługuje %1$s." "%1$s nie jest wspierany" + "Twój dostawca konta nie wspiera logowania na nowym urządzeniu za pomocą kodu QR." "Kod QR nie jest wspierany" "Logowanie zostało anulowane na drugim urządzeniu." "Prośba o logowanie została anulowana" "Logowanie wygasło. Spróbuj ponownie." "Logowanie nie zostało ukończone na czas" + "Otwórz %1$s na drugim urządzeniu" "Wybierz %1$s" + "“Zaloguj się za pomocą kodu QR”" + "Zeskanuj kod QR pokazany tutaj za pomocą drugiego urządzenia" + "Otwórz %1$s na drugim urządzeniu" + "Komputer stacjonarny" + "Ładowanie kodu QR…" + "Urządzenie mobilne" + "Jakiego typu urządzenie chcesz powiązać?" + "Spróbuj ponownie i upewnij się, że 2-cyfrowy kod został wpisany prawidłowo. Jeśli liczby wciąż się nie zgadzają, skontaktuj się ze swoim dostawcą konta." + "Liczby nie pasują do siebie" "Nie udało się nawiązać bezpiecznego połączenia z nowym urządzeniem. Twoje istniejące urządzenia są nadal bezpieczne i nie musisz się o nie martwić." "Co teraz?" "Spróbuj zalogować się ponownie za pomocą kodu QR, jeśli byłby to problem z siecią" "Jeśli napotkasz ten sam problem, użyj innej sieci Wi-FI lub danych mobilnych" "Jeśli to nie zadziała, zaloguj się ręcznie" "Połączenie nie jest bezpieczne" + "Zostaniesz poproszony o wprowadzenie dwóch cyfr widocznych na tym urządzeniu." + "Wprowadź numer poniżej na innym urządzeniu" "Logowanie zostało anulowane na drugim urządzeniu." "Prośba o logowanie została anulowana" "Logowanie zostało odrzucone na drugim urządzeniu." "Logowanie odrzucone" + "Nie musisz już robić nic więcej." + "Twoje drugie urządzenie jest już zalogowane" "Logowanie wygasło. Spróbuj ponownie." "Logowanie nie zostało ukończone na czas" "Twoje drugie urządzenie nie wspiera logowania się do %s za pomocą kodu QR. @@ -35,4 +56,5 @@ Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu.""Musisz przyznać uprawnienia %1$s do korzystania z kamery, aby kontynuować." "Zezwól na dostęp do kamery, aby zeskanować kod QR" "Wystąpił nieoczekiwany błąd. Spróbuj ponownie." + "Oczekiwanie na drugie urządzenie" diff --git a/features/linknewdevice/impl/src/main/res/values-pt-rBR/translations.xml b/features/linknewdevice/impl/src/main/res/values-pt-rBR/translations.xml index f11bdc6e6d..1680e5c0ff 100644 --- a/features/linknewdevice/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-pt-rBR/translations.xml @@ -34,6 +34,8 @@ "Se o problema persistir, tente uma rede Wi-Fi diferente ou use seus dados móveis em vez de Wi-Fi" "Se isso não funcionar, entre manualmente" "Conexão insegura" + "Você será solicitado a inserir os dois dígitos mostrados neste dispositivo." + "Digite o número abaixo no seu outro dispositivo" "A entrada foi cancelada no outro dispositivo." "Solicitação de entrada foi cancelada" "A entrada foi recusada no outro dispositivo." @@ -54,4 +56,5 @@ Tente entrar manualmente ou ler o código QR com outro dispositivo." "Você deve permitir que o %1$s use a câmera do seu dispositivo para continuar." "Permita o acesso à câmera para ler o código QR" "Ocorreu um erro inesperado. Tente novamente." + "Aguardando seu outro dispositivo" diff --git a/features/linknewdevice/impl/src/main/res/values-pt/translations.xml b/features/linknewdevice/impl/src/main/res/values-pt/translations.xml index da6da08f38..eff27b17f0 100644 --- a/features/linknewdevice/impl/src/main/res/values-pt/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-pt/translations.xml @@ -17,6 +17,8 @@ "Se tiveres o mesmo problema, experimenta uma rede Wi-Fi diferente ou utiliza os teus dados móveis." "Se isso não funcionar, inicia sessão manualmente" "Ligação insegura" + "Ser-te-á pedido que insiras os dois dígitos indicados neste dispositivo." + "Insere o número abaixo no teu dispositivo" "O início de sessão foi cancelado no outro dispositivo." "Pedido de início de sessão cancelado" "O início de sessão foi rejeitado no outro dispositivo." @@ -35,4 +37,5 @@ Tenta iniciar a sessão manualmente ou digitaliza o código QR com outro disposi "Para continuar, tens que dar permissão à %1$s para aceder à câmara do teu dispositivo." "Permitir o acesso à câmara para ler o código QR" "Ocorreu um erro inesperado. Tenta novamente." + "À espera do teu outro dispositivo" diff --git a/features/linknewdevice/impl/src/main/res/values-ro/translations.xml b/features/linknewdevice/impl/src/main/res/values-ro/translations.xml index f1a4f3db59..57d439240d 100644 --- a/features/linknewdevice/impl/src/main/res/values-ro/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ro/translations.xml @@ -26,6 +26,7 @@ "Se încarcă codul QR…" "Dispozitiv mobil" "Ce tip de dispozitiv doriți să conectați?" + "Încercați din nou și asigurați-vă că ați introdus corect codul de 2 cifre. Dacă numerele tot nu se potrivesc, contactați furnizorul contului." "Numerele nu se potrivesc" "Nu a putut fi făcută o conexiune sigură la noul dispozitiv. Dispozitivele existente sunt încă în siguranță și nu trebuie să vă faceți griji cu privire la ele." "Și acum?" @@ -33,10 +34,14 @@ "Dacă întâmpinați aceeași problemă, încercați o altă rețea Wi-Fi sau utilizați datele mobile în loc de Wi-Fi." "Dacă nu funcționează, conectați-vă manual" "Conexiunea nu este sigură" + "Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv." + "Introduceți numărul de mai jos pe celălalt dispozitiv" "Autentificarea a fost anulată de pe celălalt dispozitiv." "Cererea de autentificare a fost anulată" "Autentificarea a fost refuzată pe celălalt dispozitiv." "Autentificarea a fost refuzată" + "Nu trebuie să faceți nimic altceva." + "Celălalt dispozitiv este deja conectat" "Autentificarea a expirat. Vă rugăm să încercați din nou." "Autentificarea nu a fost finalizată la timp" "Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR. @@ -51,4 +56,5 @@ "Trebuie să acordați permisiunea ca %1$s să folosească camera dispozitivului pentru a continua." "Permiteți accesul la cameră pentru a scana codul QR" "A apărut o eroare neașteptată. Vă rugăm să încercați din nou." + "În așteptarea celuilalt dispozitiv" diff --git a/features/linknewdevice/impl/src/main/res/values-ru/translations.xml b/features/linknewdevice/impl/src/main/res/values-ru/translations.xml index 39506417b6..1382829c04 100644 --- a/features/linknewdevice/impl/src/main/res/values-ru/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ru/translations.xml @@ -15,7 +15,7 @@ "QR-код не поддерживается" "Вход на другом устройстве был отменен." "Запрос на вход отменен" - "Срок действия входа истек. Пожалуйста, попробуйте еще раз." + "Срок действия входа истек. Пожалуйста, попробуйте ещё раз." "Вход в систему не был выполнен вовремя" "Откройте %1$s на другом устройстве" "Выберите %1$s" @@ -26,7 +26,7 @@ "Загрузка QR-кода…" "Смартфон" "Какое устройство вы хотите привязать?" - "Пожалуйста, попробуйте еще раз и убедитесь, что вы правильно ввели 2-значный код. Если цифры по-прежнему не совпадают, обратитесь к администратору сервера." + "Пожалуйста, попробуйте ещё раз и убедитесь, что Вы правильно ввели 2-значный код. Если цифры по-прежнему не совпадают, обратитесь к администратору сервера." "Цифры не совпадают" "Не удалось установить безопасное соединение с новым устройством. Не беспокойтесь, существующие устройства в безопасности." "Что теперь?" @@ -34,13 +34,15 @@ "Если вы столкнулись с той же проблемой, попробуйте сменить точку доступа Wi-Fi или используйте мобильные данные" "Если это не помогло, войдите вручную" "Соединение не защищено" + "Вам нужно будет ввести две цифры, показанные на этом устройстве." + "Введите показанный номер на своем другом устройстве" "Вход на другом устройстве был отменен." "Запрос на вход отменен" "Вход в систему был отклонен на другом устройстве." "Вход отклонен" - "На этом все." + "На этом всё." "Вход уже выполнен на другом устройстве" - "Срок действия входа истек. Пожалуйста, попробуйте еще раз." + "Срок действия входа истек. Пожалуйста, попробуйте ещё раз." "Вход в систему не был выполнен вовремя" "Другое устройство не поддерживает вход в %s с помощью QR-кода. @@ -53,5 +55,6 @@ "Неверный QR-код" "Чтобы продолжить, вам необходимо разрешить %1$s использовать камеру вашего устройства." "Разрешите доступ к камере для сканирования QR-кода" - "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз." + "Произошла непредвиденная ошибка. Пожалуйста, попробуйте ещё раз." + "Ожидание другого устройства" diff --git a/features/linknewdevice/impl/src/main/res/values-sk/translations.xml b/features/linknewdevice/impl/src/main/res/values-sk/translations.xml index cb430671bb..f64c01328b 100644 --- a/features/linknewdevice/impl/src/main/res/values-sk/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-sk/translations.xml @@ -34,6 +34,8 @@ "Ak narazíte na rovnaký problém, vyskúšajte inú sieť Wi-Fi alebo namiesto siete Wi-Fi použite mobilné dáta" "Ak to nefunguje, prihláste sa manuálne" "Pripojenie nie je bezpečené" + "Budete požiadaní o zadanie dvoch číslic zobrazených na tomto zariadení." + "Zadajte nižšie uvedené číslo na vašom druhom zariadení" "Prihlásenie bolo zrušené na druhom zariadení." "Žiadosť o prihlásenie bola zrušená" "Prihlásenie bolo zamietnuté na druhom zariadení." @@ -54,4 +56,5 @@ Skúste sa prihlásiť manuálne alebo naskenujte QR kód pomocou iného zariade "Ak chcete pokračovať, musíte udeliť povolenie aplikácii %1$s používať fotoaparát vášho zariadenia." "Povoľte prístup k fotoaparátu na naskenovanie QR kódu" "Vyskytla sa neočakávaná chyba. Prosím, skúste to znova." + "Čaká sa na vaše druhé zariadenie" diff --git a/features/linknewdevice/impl/src/main/res/values-sv/translations.xml b/features/linknewdevice/impl/src/main/res/values-sv/translations.xml index 8a1bef434a..8800b31bbd 100644 --- a/features/linknewdevice/impl/src/main/res/values-sv/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-sv/translations.xml @@ -17,6 +17,8 @@ "Om du stöter på samma problem, prova ett annat wifi-nätverk eller använd din mobildata istället för wifi" "Om det inte fungerar, logga in manuellt" "Anslutningen är inte säker" + "Du kommer att bli ombedd att ange de två siffrorna som visas på den här enheten." + "Ange numret nedan på din andra enhet" "Inloggningen avbröts på den andra enheten." "Inloggningsförfrågan avbröts" "Inloggningen avvisades på den andra enheten." @@ -35,4 +37,5 @@ Prova att logga in manuellt eller skanna QR-koden med en annan enhet." "Du måste ge tillstånd för %1$s att använda enhetens kamera för att kunna fortsätta." "Tillåt kameraåtkomst för att skanna QR-koden" "Ett oväntat fel inträffade. Vänligen försök igen." + "Väntar på din andra enhet" diff --git a/features/linknewdevice/impl/src/main/res/values-tr/translations.xml b/features/linknewdevice/impl/src/main/res/values-tr/translations.xml index e5fd989c0b..3c8767f64c 100644 --- a/features/linknewdevice/impl/src/main/res/values-tr/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-tr/translations.xml @@ -33,6 +33,8 @@ "Aynı sorunla karşılaşırsanız, farklı bir wifi ağı deneyin veya wifi yerine mobil verinizi kullanın" "Bu işe yaramazsa, manuel olarak oturum açın" "Bağlantı güvenli değil" + "Bu cihazda gösterilen iki haneyi girmeniz istenecektir." + "Aşağıdaki numarayı diğer cihazınıza girin" "Oturum açma işlemi diğer cihazda iptal edildi." "Oturum açma isteği iptal edildi" "Diğer cihazda oturum açma işlemi reddedildi." @@ -51,4 +53,5 @@ Manuel olarak oturum açmayı deneyin veya QR kodunu başka bir cihazla tarayın "Devam etmek için %1$s cihazınızın kamerasını kullanmasına izin vermeniz gerekir." "QR kodunu taramak için kamera erişimine izin verin" "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin." + "Diğer cihazınız bekleniyor" diff --git a/features/linknewdevice/impl/src/main/res/values-uk/translations.xml b/features/linknewdevice/impl/src/main/res/values-uk/translations.xml index 875b5aed16..752b5ead3f 100644 --- a/features/linknewdevice/impl/src/main/res/values-uk/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-uk/translations.xml @@ -26,6 +26,7 @@ "Завантаження QR-коду…" "Мобільний пристрій" "Який тип пристрою ви хочете під\'єднати?" + "Спробуйте ще раз і переконайтеся, що ви правильно ввели двозначний код. Якщо цифри все одно не збігаються, зверніться до свого провайдера облікового запису." "Цифри не збігаються" "Не вдалося встановити безпечне з\'єднання з новим пристроєм. Ваші наявні пристрої досі в безпеці, і вам не потрібно про них турбуватися." "Що тепер?" @@ -33,6 +34,8 @@ "Якщо ви зіткнулися з тією ж проблемою, спробуйте іншу мережу Wi-Fi або використовуйте мобільний інтернет замість Wi-Fi" "Якщо це не спрацює, увійдіть вручну" "З\'єднання не безпечне" + "Вас попросять ввести дві цифри, показані на цьому пристрої." + "Введіть номер нижче на іншому пристрої" "Вхід було скасовано на іншому пристрої." "Запит на вхід скасовано" "Вхід був відхилений на іншому пристрої." @@ -53,4 +56,5 @@ "Вам потрібно дати дозвіл %1$s на використання камери вашого пристрою, щоб продовжити." "Надайте доступ до камери, щоб сканувати QR-код" "Сталася несподівана помилка. Будь ласка, спробуйте ще раз." + "Чекаємо на ваш інший пристрій" diff --git a/features/linknewdevice/impl/src/main/res/values-ur/translations.xml b/features/linknewdevice/impl/src/main/res/values-ur/translations.xml index 54d2c2e401..0b4bb02226 100644 --- a/features/linknewdevice/impl/src/main/res/values-ur/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-ur/translations.xml @@ -17,6 +17,8 @@ "اگر آپ کو بھی یہی مسئلہ درپیش ہو، تو کوئی دوسرا وائی فائی شبکہ آزمائیں یا وائی فائی کے بجائے اپنے محمول بیانات استعمال کریں۔" "اگر یہ کام نہ کرے، تو دستی طور پر داخل ہوں" "اتصال محفوظ نہیں" + "آپ سے اس آلے پر دکھائے گئے دو ہندسوں کو درج کرنے کو کہا جائے گا۔" + "اپنے دوسرے آلے پر درج ذیل نمبر درج کریں" "دوسرے آلے پر دخول منسوخ کر دیا گیا تھا۔" "دخول کی درخواست منسوخ" "دوسرے آلہ پر دخول کو مسترد کر دیا گیا تھا۔" @@ -35,4 +37,5 @@ "جاری رکھنے کے لیے آپ %1$s کو اپنے آلے کا تصویرگر استعمال کرنے کی اجازت دینے کی ضرورت ہے۔" "کیو آر رمز کو مسح ضوئی کرنے کے لئے تصویرگر تک رسائی کی اجازت دیں" "ایک غیر متوقع نقص واقع ہوا۔ برائے مہربانی دوبارہ کوشش کریں۔" + "آپکے دوسرے آلے کا منتظر" diff --git a/features/linknewdevice/impl/src/main/res/values-uz/translations.xml b/features/linknewdevice/impl/src/main/res/values-uz/translations.xml index ed08ee1a04..1d436b5f4a 100644 --- a/features/linknewdevice/impl/src/main/res/values-uz/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-uz/translations.xml @@ -34,6 +34,8 @@ "Xuddi shu muammoga duch kelsangiz, boshqa wifi tarmogʻini sinang yoki wifi oʻrniga mobil internetdan foydalaning" "Agar bunisi ishlamasa, oddiy usulda kiring" "Ulanish xavfsiz emas" + "Sizdan ushbu qurilmada koʻrsatilgan ikkita raqamni kiritish soʻraladi." + "Narigi qurilmada quyidagi raqamni kiriting" "Boshqa qurilmadan hisobga kirish bekor qilindi." "Tizimga kirish soʻrovi bekor qilindi" "Boshqa qurilmadan hisobga kirish bekor qilindi." @@ -54,4 +56,5 @@ Oddiy usulda kiring yoki boshqa qurilma bilan QR kodni skanerlang." "Davom etish uchun %1$s qurilmangiz kamerasidan foydalanishiga ruxsat berishingiz kerak." "QR kodni skanerlash uchun kameraga ruxsat bering" "Kutilmagan xatolik yuz berdi. Qayta urining." + "Boshqa qurilmangiz kutilmoqda" diff --git a/features/linknewdevice/impl/src/main/res/values-zh-rTW/translations.xml b/features/linknewdevice/impl/src/main/res/values-zh-rTW/translations.xml index 3aa047125a..e18fbc13b6 100644 --- a/features/linknewdevice/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-zh-rTW/translations.xml @@ -34,6 +34,8 @@ "如果遇到相同的問題,請嘗試使用其他 wifi 網路或您的行動數據" "若無法運作,請手動登入" "連線不安全" + "系統會要求您輸入此裝置上顯示的兩位數字。" + "在您的其他裝置上輸入以下數字" "已在其他裝置上取消登入。" "已取消登入請求" "其他裝置拒絕登入。" @@ -54,4 +56,5 @@ "您必須授予 %1$s 權限以使用裝置相機才能繼續。" "允許相機權限以掃描 QR code" "發生意外錯誤。請再試一次。" + "等待您的其他裝置" 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 1bd74d5104..f11f60dd61 100644 --- a/features/linknewdevice/impl/src/main/res/values-zh/translations.xml +++ b/features/linknewdevice/impl/src/main/res/values-zh/translations.xml @@ -9,7 +9,7 @@ "输入两位数字的代码" "这将验证你与其它设备的连接是否安全。" "请输入另一台设备上显示的数字" - "账户提供方不支持 %1$s." + "账户提供者不支持 %1$s." "不支持 %1$s." "你的账户提供者不支持使用二维码登录到新设备。" "二维码不受支持" @@ -34,6 +34,8 @@ "如果遇到同样的问题,请尝试使用不同的 WiFi 网络或使用移动数据代替 WiFi" "如果不起作用,请手动登录" "连接不安全" + "你将被要求输入此设备上显示的两位数字。" + "在你的其它设备上输入以下数字" "登录被另一台设备取消" "登录请求已取消" "另一设备上的登录请求已被拒绝。" @@ -46,7 +48,7 @@ 尝试手动或使用另一个设备扫描二维码." "二维码不受支持" - "账户提供方不支持 %1$s." + "账户提供者不支持 %1$s." "不支持 %1$s." "使用其它设备上显示的二维码。" "重试" @@ -54,4 +56,5 @@ "你需要授予 %1$s 使用设备摄像头的权限才能继续。" "允许访问摄像头以扫描二维码" "发生了意外错误。请再试一次。" + "正在等待其它设备" diff --git a/features/linknewdevice/impl/src/main/res/values/localazy.xml b/features/linknewdevice/impl/src/main/res/values/localazy.xml index 321b168751..6ffcce227a 100644 --- a/features/linknewdevice/impl/src/main/res/values/localazy.xml +++ b/features/linknewdevice/impl/src/main/res/values/localazy.xml @@ -34,6 +34,8 @@ "If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi" "If that doesn’t work, sign in manually" "Connection not secure" + "You’ll be asked to enter the two digits shown on this device." + "Enter the number below on your other device" "The sign in was cancelled on the other device." "Sign in request cancelled" "The sign in was declined on the other device." @@ -54,4 +56,5 @@ Try signing in manually, or scan the QR code with another device." "You need to give permission for %1$s to use your device’s camera in order to continue." "Allow camera access to scan the QR code" "An unexpected error occurred. Please try again." + "Waiting for your other device" diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt index 2957a89495..a198bc27d9 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/DefaultLinkNewDeviceEntryPointTest.kt @@ -11,7 +11,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint +import io.element.android.features.lockscreen.test.FakeDeviceUnlockEntryPoint import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode @@ -37,6 +39,8 @@ class DefaultLinkNewDeviceEntryPointTest { sessionCoroutineScope = backgroundScope, linkNewMobileHandler = LinkNewMobileHandler(client), linkNewDesktopHandler = LinkNewDesktopHandler(client), + sessionEnterpriseService = FakeSessionEnterpriseService(), + deviceUnlockEntryPoint = FakeDeviceUnlockEntryPoint(), ) } val callback: LinkNewDeviceEntryPoint.Callback = object : LinkNewDeviceEntryPoint.Callback { diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt index ac0a129f49..cdb1cb6860 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeViewTest.kt @@ -5,12 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.desktop 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.linknewdevice.impl.R import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder @@ -18,42 +20,36 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class DesktopNoticeViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class DesktopNoticeViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aDesktopNoticeState(), onBackClicked = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() { + fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aDesktopNoticeState(), onBackClicked = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `when can continue - calls the expected callback`() { + fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aDesktopNoticeState(canContinue = true), onReadyToScanClick = callback, ) @@ -61,16 +57,16 @@ class DesktopNoticeViewTest { } @Test - fun `on submit button clicked - emits the Continue event`() { + fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setView( + setView( state = aDesktopNoticeState(eventSink = eventRecorder), ) - rule.clickOn(R.string.screen_link_new_device_desktop_submit) + clickOn(R.string.screen_link_new_device_desktop_submit) eventRecorder.assertSingle(DesktopNoticeEvent.Continue) } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( state: DesktopNoticeState, onBackClicked: () -> Unit = EnsureNeverCalled(), onReadyToScanClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt index aa52a70149..74619c1b71 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorViewTest.kt @@ -5,58 +5,54 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.error 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ErrorViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class ErrorViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the onCancel callback`() { + fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setErrorView( + setErrorView( onCancel = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on try again button clicked - calls the expected callback`() { + fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setErrorView( + setErrorView( onRetry = callback ) - rule.clickOn(CommonStrings.action_try_again) + clickOn(CommonStrings.action_try_again) } } @Test - fun `on cancel button clicked - calls the expected callback`() { + fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setErrorView( + setErrorView( onCancel = callback ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeTestRule.setErrorView( + private fun AndroidComposeUiTest.setErrorView( onRetry: () -> Unit = EnsureNeverCalled(), onCancel: () -> Unit = EnsureNeverCalled(), errorScreenType: ErrorScreenType = ErrorScreenType.UnknownError, diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberPresenterTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberPresenterTest.kt index fe2e11e95c..b3602c81dc 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberPresenterTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberPresenterTest.kt @@ -88,6 +88,7 @@ class EnterNumberPresenterTest { navigator = navigator, linkNewMobileHandler = linkNewMobileHandler, ).test { + skipItems(1) val initialState = awaitItem() linkMobileHandler.emitStep( LinkMobileStep.QrScanned(checkCodeSender) @@ -96,7 +97,7 @@ class EnterNumberPresenterTest { initialState.eventSink(EnterNumberEvent.UpdateNumber("88")) skipItems(1) initialState.eventSink(EnterNumberEvent.Continue) - skipItems(1) + skipItems(2) val finalState = awaitItem() assertThat(finalState.sendingCode.isLoading()).isTrue() advanceUntilIdle() @@ -130,6 +131,7 @@ class EnterNumberPresenterTest { LinkMobileStep.QrScanned(checkCodeSender) ) runCurrent() + skipItems(1) initialState.eventSink(EnterNumberEvent.UpdateNumber("88")) skipItems(1) initialState.eventSink(EnterNumberEvent.Continue) @@ -163,6 +165,7 @@ class EnterNumberPresenterTest { createPresenter( linkNewMobileHandler = linkNewMobileHandler, ).test { + skipItems(1) val initialState = awaitItem() linkMobileHandler.emitStep( LinkMobileStep.QrScanned(checkCodeSender) @@ -171,7 +174,7 @@ class EnterNumberPresenterTest { initialState.eventSink(EnterNumberEvent.UpdateNumber("88")) skipItems(1) initialState.eventSink(EnterNumberEvent.Continue) - skipItems(1) + skipItems(2) val loadingState = awaitItem() assertThat(loadingState.sendingCode.isLoading()).isTrue() expectNoEvents() diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt index 20e1d898dd..0d6da84241 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberViewTest.kt @@ -5,14 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.number import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder @@ -20,65 +22,59 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class EnterNumberViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class EnterNumberViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aEnterNumberState(), onBackClicked = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() { + fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( state = aEnterNumberState(), onBackClicked = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `on continue button clicked - emits the Continue event`() { + fun `on continue button clicked - emits the Continue event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setView( + setView( state = aEnterNumberState( number = "12", eventSink = eventRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventRecorder.assertSingle(EnterNumberEvent.Continue) } @Test - fun `when the number is not complete, continue button is disabled`() { + fun `when the number is not complete, continue button is disabled`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( state = aEnterNumberState( number = "1", eventSink = eventRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsNotEnabled() } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( state: EnterNumberState, onBackClicked: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenterTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenterTest.kt new file mode 100644 index 0000000000..f92cf66102 --- /dev/null +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodePresenterTest.kt @@ -0,0 +1,100 @@ +/* + * 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.linknewdevice.impl.screens.qrcode + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.linknewdevice.impl.LinkNewMobileHandler +import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler +import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.linknewdevice.FakeLinkMobileHandler +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.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class ShowQrCodePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + createPresenter().test { + val initialState = awaitItem() + assertThat(initialState.data.dataOrNull()).isEqualTo("DATA") + } + } + + @Test + fun `present - when handler emits QrRotating, the presenter requests to rotate the QrCode`() = runTest { + val linkMobileHandler = FakeLinkMobileHandler( + startResult = {}, + ) + val createLinkMobileHandlerResult = lambdaRecorder> { + Result.success(linkMobileHandler) + } + val matrixClient = FakeMatrixClient( + sessionCoroutineScope = backgroundScope, + createLinkMobileHandlerResult = createLinkMobileHandlerResult, + ) + val linkNewMobileHandler = LinkNewMobileHandler(matrixClient) + linkNewMobileHandler.createAndStartNewHandler() + createPresenter( + linkNewMobileHandler = linkNewMobileHandler, + ).test { + awaitItem() + linkMobileHandler.emitStep( + LinkMobileStep.QrRotating + ) + runCurrent() + val finalState = awaitItem() + assertThat(finalState.data.isLoading()).isTrue() + createLinkMobileHandlerResult.assertions().isCalledExactly(2) + } + } + + @Test + fun `present - when handler emits QrRotating, the presenter requests to rotate the QrCode and the code is rotated`() = runTest { + val linkMobileHandler = FakeLinkMobileHandler( + startResult = {}, + ) + val matrixClient = FakeMatrixClient( + sessionCoroutineScope = backgroundScope, + createLinkMobileHandlerResult = { Result.success(linkMobileHandler) }, + ) + val linkNewMobileHandler = LinkNewMobileHandler(matrixClient) + linkNewMobileHandler.createAndStartNewHandler() + createPresenter( + linkNewMobileHandler = linkNewMobileHandler, + ).test { + awaitItem() + linkMobileHandler.emitStep( + LinkMobileStep.QrRotating + ) + runCurrent() + linkMobileHandler.emitStep( + LinkMobileStep.QrReady("DATA2") + ) + val finalState = awaitItem() + assertThat(finalState.data.dataOrNull()).isEqualTo("DATA2") + } + } + + private fun createPresenter( + linkNewMobileHandler: LinkNewMobileHandler = LinkNewMobileHandler(FakeMatrixClient()), + ) = ShowQrCodePresenter( + initialData = "DATA", + linkNewMobileHandler = linkNewMobileHandler, + ) +} diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt index c6c89ba818..6715959e0e 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeViewTest.kt @@ -5,41 +5,37 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.qrcode 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ShowQrCodeViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class ShowQrCodeViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setView( + setView( onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( onBackClick: () -> Unit = EnsureNeverCalled(), ) { setContent { ShowQrCodeView( - data = "DATA", + state = aShowQrCodeState(), onBackClick = onBackClick, ) } diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenterTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenterTest.kt index 88a68786f3..d2216c2737 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenterTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootPresenterTest.kt @@ -75,15 +75,48 @@ class LinkNewDeviceRootPresenterTest { sessionCoroutineScope = backgroundScope, createLinkMobileHandlerResult = { Result.success(linkMobileHandler) } ) + val linkNewMobileHandler = LinkNewMobileHandler(matrixClient) createPresenter( matrixClient = matrixClient, + linkNewMobileHandler = linkNewMobileHandler, ).test { skipItems(1) val initialState = awaitItem() assertThat(initialState.isSupported.dataOrNull()).isTrue() - initialState.eventSink(LinkNewDeviceRootEvent.LinkMobileDevice) + linkNewMobileHandler.createAndStartNewHandler() + skipItems(1) val loadingState = awaitItem() assertThat(loadingState.qrCodeData.isLoading()).isTrue() + skipItems(1) + } + } + + @Test + fun `present - close dialog resets qrCodeData`() = runTest { + val fakeLinkMobileHandler = FakeLinkMobileHandler(startResult = {}) + val matrixClient = FakeMatrixClient( + canLinkNewDeviceResult = { Result.success(true) }, + sessionCoroutineScope = backgroundScope, + createLinkMobileHandlerResult = { Result.success(fakeLinkMobileHandler) } + ) + val linkNewMobileHandler = LinkNewMobileHandler(matrixClient) + createPresenter( + matrixClient = matrixClient, + linkNewMobileHandler = linkNewMobileHandler, + ).test { + skipItems(1) + linkNewMobileHandler.onTooManyRotation() + var errorState = awaitItem() + while (!errorState.qrCodeData.isFailure()) { + errorState = awaitItem() + } + assertThat(errorState.qrCodeData.isFailure()).isTrue() + errorState.eventSink(LinkNewDeviceRootEvent.CloseDialog) + var resetState = awaitItem() + while (!resetState.qrCodeData.isUninitialized()) { + resetState = awaitItem() + } + assertThat(resetState.qrCodeData.isUninitialized()).isTrue() } } diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt index e352debfb0..fd75a4d560 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootViewTest.kt @@ -5,97 +5,97 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.root 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.linknewdevice.impl.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam 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.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class LinkNewDeviceRootViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class LinkNewDeviceRootViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the onRetry callback`() { + fun `on back pressed - calls the onRetry callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLinkNewDeviceRootView( + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( eventSink = eventRecorder, ), onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `link desktop button clicked - calls the expected callback`() { + fun `link desktop button clicked - calls the expected callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - rule.setLinkNewDeviceRootView( + ensureCalledOnceWithParam(LinkDeviceType.Desktop) { callback -> + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(true), eventSink = eventRecorder, ), - onLinkDesktopDeviceClick = callback, + onUnlockDevice = callback, ) - rule.clickOn(R.string.screen_link_new_device_root_desktop_computer) + clickOn(R.string.screen_link_new_device_root_desktop_computer) } } @Test - fun `link mobile button clicked - emits the expected event`() { - val eventRecorder = EventsRecorder() - rule.setLinkNewDeviceRootView( - state = aLinkNewDeviceRootState( - isSupported = AsyncData.Success(true), - eventSink = eventRecorder, + fun `link mobile button clicked - calls the expected callback`() = runAndroidComposeUiTest { + val eventRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(LinkDeviceType.Mobile) { callback -> + setLinkNewDeviceRootView( + state = aLinkNewDeviceRootState( + isSupported = AsyncData.Success(true), + eventSink = eventRecorder, + ), + onUnlockDevice = callback, ) - ) - rule.clickOn(R.string.screen_link_new_device_root_mobile_device) - eventRecorder.assertSingle(LinkNewDeviceRootEvent.LinkMobileDevice) + clickOn(R.string.screen_link_new_device_root_mobile_device) + } } @Test - fun `not supported - dismiss click - invokes the expected callback`() { + fun `not supported - dismiss click - invokes the expected callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLinkNewDeviceRootView( + setLinkNewDeviceRootView( state = aLinkNewDeviceRootState( isSupported = AsyncData.Success(false), eventSink = eventRecorder, ), onBackClick = callback, ) - rule.clickOn(CommonStrings.action_dismiss) + clickOn(CommonStrings.action_dismiss) } } - private fun AndroidComposeTestRule.setLinkNewDeviceRootView( + private fun AndroidComposeUiTest.setLinkNewDeviceRootView( state: LinkNewDeviceRootState = aLinkNewDeviceRootState(), onBackClick: () -> Unit = EnsureNeverCalled(), - onLinkDesktopDeviceClick: () -> Unit = EnsureNeverCalled(), + onUnlockDevice: (type: LinkDeviceType) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { LinkNewDeviceRootView( state = state, onBackClick = onBackClick, - onLinkDesktopDeviceClick = onLinkDesktopDeviceClick, + onUnlockDevice = onUnlockDevice, ) } } diff --git a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt index fcc3afeb7d..5daf78660a 100644 --- a/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt +++ b/features/linknewdevice/impl/src/test/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeViewTest.kt @@ -5,12 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.linknewdevice.impl.screens.scan 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 androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.ui.strings.CommonStrings @@ -19,52 +23,48 @@ 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.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ScanQrCodeViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class ScanQrCodeViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( state = aScanQrCodeState( eventSink = eventRecorder, ), onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `try again button clicked - emits the expected event`() { + fun `try again button clicked - emits the expected event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setView( + setView( state = aScanQrCodeState( scanAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventRecorder, ) ) - rule.clickOn(CommonStrings.action_try_again) + clickOn(CommonStrings.action_try_again) eventRecorder.assertSingle(ScanQrCodeEvent.TryAgain) } - private fun AndroidComposeTestRule.setView( + private fun AndroidComposeUiTest.setView( state: ScanQrCodeState = aScanQrCodeState(), onBackClick: () -> Unit = EnsureNeverCalled(), ) { setContent { - ScanQrCodeView( - state = state, - onBackClick = onBackClick, - ) + CompositionLocalProvider(LocalInspectionMode provides true) { + ScanQrCodeView( + state = state, + onBackClick = onBackClick, + ) + } } } } diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index ab85e37594..f8377389f1 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) implementation(libs.coil.compose) + implementation(libs.datetime) testCommonDependencies(libs) } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LiveLocationSharingBanner.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LiveLocationSharingBanner.kt new file mode 100644 index 0000000000..8c53550737 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LiveLocationSharingBanner.kt @@ -0,0 +1,100 @@ +/* + * 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.location.api + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +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.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.ButtonSize +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LiveLocationSharingBanner( + onClick: () -> Unit, + onStopClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(ElementTheme.colors.bgCanvasDefault) + .drawBannerBorder(ElementTheme.colors.separatorPrimary) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = CompoundIcons.LocationPinSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconAccentPrimary, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringResource(CommonStrings.screen_room_live_location_banner), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textPrimary, + ) + } + Button( + text = stringResource(CommonStrings.action_stop), + onClick = onStopClick, + destructive = true, + size = ButtonSize.Small, + ) + } +} + +private fun Modifier.drawBannerBorder(borderColor: Color): Modifier = drawBehind { + val strokeWidth = 1.dp.toPx() + val bottomY = size.height - strokeWidth / 2 + drawLine( + color = borderColor, + start = Offset(0f, strokeWidth / 2), + end = Offset(size.width, strokeWidth / 2), + strokeWidth = strokeWidth, + ) + drawLine( + color = borderColor, + start = Offset(0f, bottomY), + end = Offset(size.width, bottomY), + strokeWidth = strokeWidth, + ) +} + +@PreviewsDayNight +@Composable +internal fun LiveLocationSharingBannerPreview() = ElementPreview { + LiveLocationSharingBanner( + onClick = {}, + onStopClick = {}, + ) +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/RenderingMapsNotSupportedDialog.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/RenderingMapsNotSupportedDialog.kt new file mode 100644 index 0000000000..a4d65763ab --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/RenderingMapsNotSupportedDialog.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.location.api + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +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.libraries.ui.strings.CommonStrings + +@Composable +fun RenderingMapsNotSupportedDialog(onSubmit: () -> Unit) { + ErrorDialog( + title = stringResource(CommonStrings.vulkan_not_supported_dialog_title_android), + content = stringResource(CommonStrings.vulkan_not_supported_dialog_content_android), + onSubmit = onSubmit, + ) +} + +@PreviewsDayNight +@Composable +internal fun RenderingMapsNotSupportedDialogPreview() = ElementPreview { + RenderingMapsNotSupportedDialog(onSubmit = {}) +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt index a61cbe1c24..c16b1ae560 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -8,6 +8,7 @@ package io.element.android.features.location.api +import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -24,13 +25,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.graphics.createBitmap import coil3.Extras +import coil3.asImage import coil3.compose.AsyncImagePainter import coil3.compose.rememberAsyncImagePainter import coil3.request.ImageRequest +import coil3.request.SuccessResult import io.element.android.compound.theme.ElementTheme import io.element.android.features.location.api.internal.StaticMapPlaceholder import io.element.android.features.location.api.internal.StaticMapUrlBuilder @@ -39,6 +44,7 @@ import io.element.android.libraries.designsystem.components.LocationPin import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.utils.CommonDrawables /** * Shows a static map image downloaded via a third party service's static maps API. @@ -128,32 +134,46 @@ private fun BoxWithConstraintsScope.LoadableMapContent( var retryHash by remember { mutableIntStateOf(0) } val builder = remember { StaticMapUrlBuilder() } - val painter = rememberAsyncImagePainter( - model = if (constraints.isZero) { - // Avoid building a URL if any of the size constraints is zero - null - } else { - ImageRequest.Builder(context) - .data( - builder.build( - lat = location.lat, - lon = location.lon, - zoom = zoom, - darkMode = darkMode, - width = constraints.maxWidth, - height = constraints.maxHeight, - density = LocalDensity.current.density, + val (painter, state, contentScale) = if (LocalInspectionMode.current) { + val painter = painterResource(CommonDrawables.sample_map) + val state = AsyncImagePainter.State.Success( + painter = painter, + result = SuccessResult( + image = createBitmap(1, 1, Bitmap.Config.ALPHA_8).asImage(), + request = ImageRequest.Builder(context).build() + ) + ) + Triple(painter, state, ContentScale.Crop) + } else { + val painter = rememberAsyncImagePainter( + model = if (constraints.isZero) { + // Avoid building a URL if any of the size constraints is zero + null + } else { + ImageRequest.Builder(context) + .data( + builder.build( + lat = location.lat, + lon = location.lon, + zoom = zoom, + darkMode = darkMode, + width = constraints.maxWidth, + height = constraints.maxHeight, + density = LocalDensity.current.density, + ) ) - ) - .size(width = constraints.maxWidth, height = constraints.maxHeight) - .apply { - extras.set(Extras.Key("retry_hash"), retryHash).build() - } - .build() - } - ) + .size(width = constraints.maxWidth, height = constraints.maxHeight) + .apply { + extras.set(Extras.Key("retry_hash"), retryHash).build() + } + .build() + } + ) + + val state by painter.state.collectAsState() + Triple(painter, state, ContentScale.Fit) + } - val state by painter.state.collectAsState() when (state) { is AsyncImagePainter.State.Success -> { Image( @@ -162,7 +182,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent( modifier = Modifier.size(width = maxWidth, height = maxHeight), // The returned image can be smaller than the requested size due to the static maps API having // a max width and height of 2048 px. We apply ContentScale.Fit to handle this. - contentScale = ContentScale.Fit, + contentScale = contentScale, ) LocationPin(variant = pinVariant, modifier = Modifier.centerBottomEdge(this)) } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt index d119dc7eb2..054805d1ab 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt @@ -25,8 +25,18 @@ internal class MapTilerTileServerStyleUriBuilder( darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) - override fun build(darkMode: Boolean): String { - val mapId = if (darkMode) darkMapId else lightMapId - return "$baseUrl/$mapId/style.json?key=$apiKey" + override fun build( + customMapStyleUrl: String?, + darkMode: Boolean, + ): String { + return buildString { + if (customMapStyleUrl.isNullOrBlank()) { + val mapId = if (darkMode) darkMapId else lightMapId + append("$baseUrl/$mapId/style.json") + } else { + append(customMapStyleUrl) + } + append("?key=$apiKey") + } } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt index 17b620eec9..762d580f8e 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt @@ -19,21 +19,25 @@ import io.element.android.compound.theme.ElementTheme */ interface TileServerStyleUriBuilder { fun build( + customMapStyleUrl: String?, darkMode: Boolean, ): String } -fun TileServerStyleUriBuilder(): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder() - /** * Provides and remembers a style URI for a MapLibre compatible tile server. * * Used for rendering dynamic maps. */ @Composable -fun rememberTileStyleUrl(): String { +fun rememberTileStyleUrl( + customMapStyleUrl: String?, +): String { val darkMode = !ElementTheme.isLightTheme - return remember(darkMode) { - TileServerStyleUriBuilder().build(darkMode) + return remember(darkMode, customMapStyleUrl) { + MapTilerTileServerStyleUriBuilder().build( + customMapStyleUrl = customMapStyleUrl, + darkMode = darkMode, + ) } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt new file mode 100644 index 0000000000..cd6b8731c1 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt @@ -0,0 +1,43 @@ +/* + * 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.location.api.live + +import io.element.android.libraries.core.coroutine.mapState +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.StateFlow +import kotlin.time.Duration + +interface ActiveLiveLocationShareManager { + /** All rooms currently sharing live location on this device. */ + val sharingRoomIds: StateFlow> + + /** + * Initializes the manager. + * This will restart or stop current location sharing and set the listener on the SDK + * and the session manager. + */ + suspend fun setup() + + /** + * Starts live location sharing in the given room. + * Calls room.startLiveLocationShare() on the SDK, registers the share, + * and starts the foreground GPS service if not already running. + */ + suspend fun startShare(roomId: RoomId, duration: Duration): Result + + /** + * Stops live location sharing in the given room. + * Calls room.stopLiveLocationShare() on the SDK, removes the share, + * and stops the foreground service if no shares remain. + */ + suspend fun stopShare(roomId: RoomId): Result +} + +fun ActiveLiveLocationShareManager.isCurrentlySharing(roomId: RoomId): StateFlow { + return sharingRoomIds.mapState { roomId in it } +} diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt index d5c6521258..2e07204594 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt @@ -22,14 +22,40 @@ class MapTilerTileServerStyleUriBuilderTest { @Test fun `light map uri`() { assertThat( - builder.build(darkMode = false) + builder.build( + customMapStyleUrl = null, + darkMode = false, + ) ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") } @Test fun `dark map uri`() { assertThat( - builder.build(darkMode = true) + builder.build( + customMapStyleUrl = null, + darkMode = true, + ) ).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") } + + @Test + fun `custom map uri light`() { + assertThat( + builder.build( + customMapStyleUrl = "https://custom.url/style.json", + darkMode = false, + ) + ).isEqualTo("https://custom.url/style.json?key=anApiKey") + } + + @Test + fun `custom map uri dark`() { + assertThat( + builder.build( + customMapStyleUrl = "https://custom.url/style.json", + darkMode = true, + ) + ).isEqualTo("https://custom.url/style.json?key=anApiKey") + } } diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index 0da54a1394..165c32b7c5 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -37,10 +37,16 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.matrixui) implementation(projects.services.analytics.api) + implementation(projects.services.appnavstate.api) implementation(libs.accompanist.permission) implementation(projects.libraries.uiStrings) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.preferences.api) + implementation(projects.libraries.push.api) + implementation(projects.libraries.sessionStorage.api) + implementation(libs.androidx.datastore.preferences) + implementation(libs.datetime) testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) @@ -50,4 +56,7 @@ dependencies { testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.sessionStorage.test) + testImplementation(projects.features.location.test) } diff --git a/features/location/impl/src/main/AndroidManifest.xml b/features/location/impl/src/main/AndroidManifest.xml index ae728c09e1..e56a13e644 100644 --- a/features/location/impl/src/main/AndroidManifest.xml +++ b/features/location/impl/src/main/AndroidManifest.xml @@ -5,8 +5,26 @@ ~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. ~ Please see LICENSE files in the repository root for full details. --> - + + + + + + + + + + + diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt index a0b0cd4734..72add1ea14 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt @@ -14,15 +14,19 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi sealed interface LocationConstraintsCheck { data object Success : LocationConstraintsCheck data object PermissionRationale : LocationConstraintsCheck + data object PermissionShouldBeRequested : LocationConstraintsCheck data object PermissionDenied : LocationConstraintsCheck data object LocationServiceDisabled : LocationConstraintsCheck + data object NotEnoughPowerLevel : LocationConstraintsCheck } fun checkLocationConstraints( permissionsState: PermissionsState, locationActions: LocationActions, + sendLiveLocationPermissions: SendLiveLocationPermissions, ): LocationConstraintsCheck { return when { + !sendLiveLocationPermissions.hasAll -> LocationConstraintsCheck.NotEnoughPowerLevel permissionsState.isAnyGranted -> { if (locationActions.isLocationEnabled()) { LocationConstraintsCheck.Success @@ -31,6 +35,7 @@ fun checkLocationConstraints( } } permissionsState.shouldShowRationale -> LocationConstraintsCheck.PermissionRationale + !permissionsState.permissionsAlreadyRequested -> LocationConstraintsCheck.PermissionShouldBeRequested else -> LocationConstraintsCheck.PermissionDenied } } @@ -38,8 +43,10 @@ fun checkLocationConstraints( fun LocationConstraintsCheck.toDialogState(): LocationConstraintsDialogState { return when (this) { LocationConstraintsCheck.Success -> LocationConstraintsDialogState.None + LocationConstraintsCheck.PermissionShouldBeRequested -> LocationConstraintsDialogState.None LocationConstraintsCheck.PermissionRationale -> LocationConstraintsDialogState.PermissionRationale LocationConstraintsCheck.PermissionDenied -> LocationConstraintsDialogState.PermissionDenied LocationConstraintsCheck.LocationServiceDisabled -> LocationConstraintsDialogState.LocationServiceDisabled + LocationConstraintsCheck.NotEnoughPowerLevel -> LocationConstraintsDialogState.NotEnoughPowerLevel } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/SendLiveLocationPermissions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/SendLiveLocationPermissions.kt new file mode 100644 index 0000000000..d1a9e32026 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/SendLiveLocationPermissions.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.features.location.impl.common + +import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions + +/** + * Permissions to send beacon and beacon_info events in the room. + */ +data class SendLiveLocationPermissions( + val canSendBeacon: Boolean, + val canSendBeaconInfo: Boolean, +) { + val hasAll = canSendBeaconInfo && canSendBeacon + + companion object { + val DEFAULT = SendLiveLocationPermissions(canSendBeacon = false, canSendBeaconInfo = false) + val GRANTED = SendLiveLocationPermissions(canSendBeacon = true, canSendBeaconInfo = true) + } +} + +fun RoomPermissions.sendLiveLocationPermissions(): SendLiveLocationPermissions { + return SendLiveLocationPermissions( + canSendBeaconInfo = canOwnUserSendState(StateEventType.BeaconInfo), + canSendBeacon = canOwnUserSendMessage(MessageEventType.Beacon), + ) +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt index 1aa2e1269d..5aa7ae6f63 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt @@ -9,6 +9,10 @@ package io.element.android.features.location.impl.common.permissions 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 com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState @@ -32,11 +36,16 @@ class DefaultPermissionsPresenter( @OptIn(ExperimentalPermissionsApi::class) @Composable override fun present(): PermissionsState { - val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions) + var permissionsRequested by remember { mutableStateOf(false) } + val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions) { + permissionsRequested = true + } fun handleEvent(event: PermissionsEvents) { when (event) { - PermissionsEvents.RequestPermissions -> multiplePermissionsState.launchMultiplePermissionRequest() + PermissionsEvents.RequestPermissions -> { + multiplePermissionsState.launchMultiplePermissionRequest() + } } } @@ -47,6 +56,7 @@ class DefaultPermissionsPresenter( else -> PermissionsState.Permissions.NoneGranted }, shouldShowRationale = multiplePermissionsState.shouldShowRationale, + permissionsAlreadyRequested = permissionsRequested, eventSink = ::handleEvent, ) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt index 91191280d0..25ceb3ba29 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt @@ -11,6 +11,7 @@ package io.element.android.features.location.impl.common.permissions data class PermissionsState( val permissions: Permissions, val shouldShowRationale: Boolean, + val permissionsAlreadyRequested: Boolean, val eventSink: (PermissionsEvents) -> Unit, ) { sealed interface Permissions { diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt index 95f5129f91..334aebaee6 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt @@ -10,6 +10,8 @@ package io.element.android.features.location.impl.common.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.res.stringResource +import io.element.android.features.location.impl.R +import io.element.android.libraries.designsystem.components.dialogs.AlertDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.ui.strings.CommonStrings @@ -42,6 +44,10 @@ fun LocationConstraintsDialog( onDismiss = onDismiss, submitText = stringResource(CommonStrings.action_continue), ) + LocationConstraintsDialogState.NotEnoughPowerLevel -> AlertDialog( + content = stringResource(R.string.screen_share_location_live_location_missing_permissions), + onDismiss = onDismiss + ) } } @@ -51,4 +57,5 @@ sealed interface LocationConstraintsDialogState { data object PermissionRationale : LocationConstraintsDialogState data object PermissionDenied : LocationConstraintsDialogState data object LocationServiceDisabled : LocationConstraintsDialogState + data object NotEnoughPowerLevel : LocationConstraintsDialogState } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt index 762a719cf4..e30dfd53de 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt @@ -14,6 +14,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.features.location.api.Location import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.components.rememberLocationPinBitmap +import kotlinx.collections.immutable.ImmutableList import kotlinx.serialization.json.JsonPrimitive import org.maplibre.compose.expressions.dsl.and import org.maplibre.compose.expressions.dsl.asString @@ -66,7 +67,7 @@ data class LocationMarkerData( */ @Composable fun LocationPinMarkers( - markers: List, + markers: ImmutableList, onMarkerClick: ((LocationMarkerData) -> Unit)? = null, onClusterClick: ((Position) -> Unit)? = null, ) { diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationShareRow.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationShareRow.kt index 83db9a9c61..24476e3c66 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationShareRow.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationShareRow.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,6 +32,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB 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 @@ -42,6 +45,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LocationShareRow( item: LocationShareItem, onShareClick: () -> Unit, + onStopClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -99,11 +103,24 @@ fun LocationShareRow( ) } } + if (item.canStopSharing) { + IconButton( + onClick = onStopClick, + colors = IconButtonDefaults.iconButtonColors( + containerColor = ElementTheme.colors.bgCriticalPrimary, + contentColor = ElementTheme.colors.iconOnSolidPrimary, + ) + ) { + Icon( + imageVector = CompoundIcons.Stop(), + contentDescription = stringResource(CommonStrings.action_stop), + ) + } + } IconButton(onClick = onShareClick) { Icon( imageVector = CompoundIcons.ShareAndroid(), contentDescription = stringResource(CommonStrings.action_share), - tint = ElementTheme.colors.iconPrimary, ) } } @@ -116,35 +133,39 @@ internal fun LocationShareRowPreview() = ElementPreview { LocationShareRow( item = LocationShareItem( userId = UserId("@alice:matrix.org"), - displayName = "Alice", + displayName = USER_NAME_ALICE, avatarData = AvatarData( id = "@alice:matrix.org", - name = "Alice", + name = USER_NAME_ALICE, url = null, size = AvatarSize.UserListItem, ), formattedTimestamp = "Shared 1 min ago", isLive = true, assetType = AssetType.SENDER, - location = Location(0.0, 0.0) + location = Location(0.0, 0.0), + isOwnUser = true, ), + onStopClick = {}, onShareClick = {}, ) LocationShareRow( item = LocationShareItem( userId = UserId("@bob:matrix.org"), - displayName = "Bob", + displayName = USER_NAME_BOB, avatarData = AvatarData( id = "@bob:matrix.org", - name = "Bob", + name = USER_NAME_BOB, url = null, size = AvatarSize.UserListItem, ), isLive = false, assetType = AssetType.PIN, formattedTimestamp = "Shared 5 hours ago", - location = Location(0.0, 0.0) + location = Location(0.0, 0.0), + isOwnUser = false ), + onStopClick = {}, onShareClick = {}, ) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt index 13c30c28eb..5f09bcedb2 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp @@ -44,9 +45,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import io.element.android.features.location.api.internal.rememberTileStyleUrl import io.element.android.features.location.impl.common.MapDefaults +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MapOptions @@ -63,6 +66,7 @@ import kotlin.math.roundToInt * - Updating camera position padding based on sheet height * - Rendering the MaplibreMap with proper ornament positioning * + * @param customMapStyleUrl Optional custom style URL for the map * @param modifier Modifier for the root layout * @param scaffoldState State for the bottom sheet scaffold * @param cameraState The camera state for the map @@ -79,6 +83,7 @@ import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable fun MapBottomSheetScaffold( + customMapStyleUrl: AsyncData, modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState( bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.PartiallyExpanded) @@ -130,13 +135,22 @@ fun MapBottomSheetScaffold( val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding) val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions) Box { - MaplibreMap( - options = mapOptions, - baseStyle = BaseStyle.Uri(rememberTileStyleUrl()), - modifier = Modifier.fillMaxSize(), - cameraState = cameraState, - content = mapContent, - ) + when (customMapStyleUrl) { + is AsyncData.Success -> { + MaplibreMap( + options = mapOptions, + baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapStyleUrl.data)), + modifier = Modifier.fillMaxSize(), + cameraState = cameraState, + content = mapContent, + ) + } + else -> { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + } + } overlayContent(sheetPadding) } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt index 8b89f77be4..074404a4cc 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt @@ -7,75 +7,35 @@ package io.element.android.features.location.impl.common.ui -import android.annotation.SuppressLint import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.location.impl.common.MapDefaults import org.maplibre.compose.camera.CameraState -import org.maplibre.compose.location.DesiredAccuracy +import org.maplibre.compose.location.Location import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckSizes -import org.maplibre.compose.location.LocationTrackingEffect -import org.maplibre.compose.location.UserLocationState -import org.maplibre.compose.location.rememberAndroidLocationProvider -import org.maplibre.compose.location.rememberNullLocationProvider -import org.maplibre.compose.location.rememberUserLocationState -import kotlin.time.Duration.Companion.minutes @Composable fun UserLocationPuck( cameraState: CameraState, - locationState: UserLocationState, - trackUserLocation: Boolean, + location: Location?, ) { - LocationTrackingEffect( - locationState = locationState, - enabled = trackUserLocation, - ) { - val finalPosition = cameraState.position.copy( - target = currentLocation.position, - bearing = currentLocation.bearing ?: cameraState.position.bearing, - zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM) + LocationPuck( + idPrefix = "user-location", + location = location, + cameraState = cameraState, + accuracyThreshold = Float.POSITIVE_INFINITY, + showBearingAccuracy = false, + showBearing = false, + sizes = LocationPuckSizes( + dotRadius = 8.dp, + dotStrokeWidth = 2.dp, + ), + colors = LocationPuckColors( + dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary, + dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary, + dotStrokeColor = ElementTheme.colors.bgCanvasDefault, ) - cameraState.animateTo(finalPosition) - } - val location = locationState.location - if (location != null) { - LocationPuck( - idPrefix = "user-location", - locationState = locationState, - cameraState = cameraState, - accuracyThreshold = Float.POSITIVE_INFINITY, - showBearingAccuracy = false, - showBearing = false, - sizes = LocationPuckSizes( - dotRadius = 8.dp, - dotStrokeWidth = 2.dp, - ), - colors = LocationPuckColors( - dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary, - dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary, - dotStrokeColor = ElementTheme.colors.bgCanvasDefault, - ) - ) - } -} - -@SuppressLint("MissingPermission") -@Composable -fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState { - val isPreview = LocalInspectionMode.current - val locationProvider = if (isPreview || !hasLocationPermission) { - rememberNullLocationProvider() - } else { - rememberAndroidLocationProvider( - updateInterval = 1.minutes, - desiredAccuracy = DesiredAccuracy.Balanced, - minDistanceMeters = 50f, - ) - } - return rememberUserLocationState(locationProvider) + ) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/DefaultUserLocationStateFactory.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/DefaultUserLocationStateFactory.kt new file mode 100644 index 0000000000..01a2b92d4a --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/DefaultUserLocationStateFactory.kt @@ -0,0 +1,38 @@ +/* + * 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.location.impl.common.userlocation + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.di.RoomScope +import org.maplibre.compose.location.DesiredAccuracy +import org.maplibre.compose.location.rememberNullLocationProvider +import org.maplibre.spatialk.units.extensions.meters +import kotlin.time.Duration.Companion.seconds + +@ContributesBinding(RoomScope::class) +class DefaultUserLocationStateFactory : UserLocationState.Factory { + @Composable + override fun create(hasLocationPermission: Boolean): UserLocationState { + val locationProvider = if (!hasLocationPermission) { + rememberNullLocationProvider() + } else { + @SuppressLint("MissingPermission") + rememberPlatformLocationProvider( + updateInterval = 5.seconds, + desiredAccuracy = DesiredAccuracy.High, + minDistance = 5.meters, + ) + } + val location by locationProvider.location.collectAsState() + return UserLocationState(location) + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/Location.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/Location.kt new file mode 100644 index 0000000000..9777e14af5 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/Location.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.location.impl.common.userlocation + +import io.element.android.features.location.api.Location +import org.maplibre.compose.location.PositionWithAccuracy +import org.maplibre.spatialk.geojson.Position +import org.maplibre.spatialk.units.extensions.meters +import kotlin.time.TimeSource +import org.maplibre.compose.location.Location as MapLibreLocation + +fun Location.asMapLibreLocation(): MapLibreLocation { + return MapLibreLocation( + position = PositionWithAccuracy( + value = Position(latitude = lat, longitude = lon), + accuracy = accuracy?.toDouble()?.meters + ), + // Not relevant as not used + timestamp = TimeSource.Monotonic.markNow(), + ) +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/PlatformLocationProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/PlatformLocationProvider.kt new file mode 100644 index 0000000000..54e6d19cf6 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/PlatformLocationProvider.kt @@ -0,0 +1,129 @@ +/* + * 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.location.impl.common.userlocation + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.LocationManager +import android.os.Handler +import android.os.HandlerThread +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import androidx.core.location.LocationListenerCompat +import androidx.core.location.LocationManagerCompat +import androidx.core.location.LocationRequestCompat +import androidx.core.os.ExecutorCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.stateIn +import org.maplibre.compose.location.DesiredAccuracy +import org.maplibre.compose.location.Location +import org.maplibre.compose.location.LocationProvider +import org.maplibre.compose.location.PermissionException +import org.maplibre.compose.location.asMapLibreLocation +import org.maplibre.spatialk.units.Length +import org.maplibre.spatialk.units.extensions.inMeters +import kotlin.time.Duration + +@SuppressLint("InlinedApi") +class PlatformLocationProvider( + context: Context, + private val updateInterval: Duration, + private val minDistance: Length, + private val desiredAccuracy: DesiredAccuracy = DesiredAccuracy.High, + coroutineScope: CoroutineScope, + sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000), +) : LocationProvider { + override val location: StateFlow + + init { + if (!handlerThread.isAlive) handlerThread.start() + if ( + context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED + ) { + throw PermissionException() + } + val locationManager = context.getSystemService(LocationManager::class.java) + val provider = PROVIDERS_BY_PRIORITY.firstOrNull { LocationManagerCompat.hasProvider(locationManager, it) } + val locationFlow = if (provider != null) { + createProviderFlow(locationManager, provider) + } else { + emptyFlow() + } + location = locationFlow.stateIn(coroutineScope, sharingStarted, null) + } + + @RequiresPermission( + anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION] + ) + private fun createProviderFlow(locationManager: LocationManager, provider: String) = callbackFlow { + send(locationManager.getLastKnownLocation(provider)?.asMapLibreLocation()) + val listener = LocationListenerCompat { trySend(it.asMapLibreLocation()) } + val request = LocationRequestCompat.Builder(updateInterval.inWholeMilliseconds) + .setQuality(desiredAccuracy.toLocationRequestQuality()) + .setMinUpdateDistanceMeters(minDistance.inMeters.toFloat()) + .build() + LocationManagerCompat.requestLocationUpdates( + locationManager, + provider, + request, + ExecutorCompat.create(Handler(handlerThread.looper)), + listener, + ) + awaitClose { LocationManagerCompat.removeUpdates(locationManager, listener) } + } + + private companion object { + private val PROVIDERS_BY_PRIORITY = listOf( + LocationManager.FUSED_PROVIDER, + LocationManager.GPS_PROVIDER, + LocationManager.NETWORK_PROVIDER, + ) + private val handlerThread by lazy { HandlerThread("PlatformLocationProvider") } + } +} + +private fun DesiredAccuracy.toLocationRequestQuality(): Int = when (this) { + DesiredAccuracy.Highest, DesiredAccuracy.High -> LocationRequestCompat.QUALITY_HIGH_ACCURACY + DesiredAccuracy.Balanced -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY + DesiredAccuracy.Low, DesiredAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER +} + +@Composable +@RequiresPermission( + anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION] +) +fun rememberPlatformLocationProvider( + updateInterval: Duration, + minDistance: Length, + desiredAccuracy: DesiredAccuracy = DesiredAccuracy.High, + context: Context = LocalContext.current, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000), +): PlatformLocationProvider { + return remember(context, updateInterval, minDistance, desiredAccuracy, coroutineScope, sharingStarted) { + PlatformLocationProvider( + context = context, + updateInterval = updateInterval, + minDistance = minDistance, + desiredAccuracy = desiredAccuracy, + coroutineScope = coroutineScope, + sharingStarted = sharingStarted, + ) + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/UserLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/UserLocationState.kt new file mode 100644 index 0000000000..d9dc2037a6 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/UserLocationState.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.location.impl.common.userlocation + +import androidx.compose.runtime.Composable +import org.maplibre.compose.location.Location + +data class UserLocationState(val location: Location?) { + fun interface Factory { + @Composable + fun create(hasLocationPermission: Boolean): UserLocationState + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/UserLocationTrackingEffect.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/UserLocationTrackingEffect.kt new file mode 100644 index 0000000000..ca6c10b7fa --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/userlocation/UserLocationTrackingEffect.kt @@ -0,0 +1,77 @@ +/* + * 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.location.impl.common.userlocation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow +import io.element.android.features.location.impl.common.MapDefaults +import kotlinx.coroutines.flow.distinctUntilChanged +import org.maplibre.compose.camera.CameraState +import org.maplibre.compose.location.Location +import kotlin.math.abs + +/** + * Drop-in replacement for the library's LocationTrackingEffect. + * TODO remove once https://github.com/maplibre/maplibre-compose/issues/808 is fixed + */ +@Composable +internal fun UserLocationTrackingEffect( + locationState: UserLocationState, + enabled: Boolean = true, + precision: Double = 0.00001, + onLocationChange: suspend (Location?) -> Unit, +) { + val latestOnLocationChange by rememberUpdatedState(onLocationChange) + val latestLocationState by rememberUpdatedState(locationState) + LaunchedEffect(enabled) { + if (!enabled) return@LaunchedEffect + val locationStateFlow = snapshotFlow { latestLocationState.location } + locationStateFlow + .distinctUntilChanged { oldLocation, newLocation -> + if (oldLocation != null && newLocation != null) { + when { + abs(oldLocation.position.value.latitude - newLocation.position.value.latitude) >= precision -> false + abs(oldLocation.position.value.longitude - newLocation.position.value.longitude) >= precision -> false + else -> true + } + } else { + false + } + } + .collect { location -> + latestOnLocationChange(location) + } + } +} + +@Composable +internal fun UserLocationTrackingEffect( + cameraState: CameraState, + locationState: UserLocationState, + enabled: Boolean = true, + precision: Double = 0.00001, +) { + UserLocationTrackingEffect( + locationState = locationState, + enabled = enabled, + precision = precision + ) { location -> + val target = location?.position?.value ?: cameraState.position.target + cameraState.animateTo( + cameraState.position.copy( + target = target, + // Force pointing to NORTH + bearing = 0.0, + zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM) + ) + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/di/LocationBindings.kt similarity index 58% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/di/LocationBindings.kt index 49b2c43bc7..ee70936160 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindings.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/di/LocationBindings.kt @@ -5,13 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.push.impl.di +package io.element.android.features.location.impl.di import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesTo -import io.element.android.libraries.push.impl.push.FetchPushForegroundService +import io.element.android.features.location.impl.live.service.LiveLocationSharingService @ContributesTo(AppScope::class) -interface PushBindings { - fun inject(fetchPushForegroundService: FetchPushForegroundService) +interface LocationBindings { + fun inject(service: LiveLocationSharingService) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManager.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManager.kt new file mode 100644 index 0000000000..c24d3f9cc2 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManager.kt @@ -0,0 +1,232 @@ +/* + * 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.location.impl.live + +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding +import io.element.android.features.location.api.Location +import io.element.android.features.location.api.live.ActiveLiveLocationShareManager +import io.element.android.features.location.impl.live.service.LiveLocationReceiver +import io.element.android.features.location.impl.live.service.LiveLocationSharingCoordinator +import io.element.android.libraries.di.SessionScope +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.JoinedRoom +import io.element.android.libraries.matrix.api.room.location.BeaconId +import io.element.android.libraries.matrix.api.room.location.LiveLocationException +import io.element.android.libraries.sessionstorage.api.observer.SessionListener +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.time.Duration +import kotlin.time.Instant + +@OptIn(ExperimentalAtomicApi::class) +@SingleIn(SessionScope::class) +@ContributesBinding(SessionScope::class, binding = binding()) +class DefaultActiveLiveLocationShareManager( + private val matrixClient: MatrixClient, + private val coordinator: LiveLocationSharingCoordinator, + private val liveLocationStore: LiveLocationStore, + private val clock: SystemClock, + private val sessionObserver: SessionObserver, +) : ActiveLiveLocationShareManager, LiveLocationReceiver { + private val isSetup = AtomicBoolean(false) + private val cachedRooms = ConcurrentHashMap() + private val timeoutJobs = ConcurrentHashMap() + private val syncedActiveShareIds = MutableStateFlow>(emptySet()) + private val localSharingRoomIds = MutableStateFlow>(emptySet()) + override val sharingRoomIds: StateFlow> = localSharingRoomIds + + override suspend fun setup() = withContext(NonCancellable) { + if (isSetup.compareAndSet(expectedValue = false, newValue = true)) { + Timber.d("ActiveLiveLocationShareManager setup manager.") + + recoverPersistedShares() + + matrixClient.ownBeaconInfoUpdates + .onEach { update -> + Timber.d("Received beaconInfoUpdate:$update") + // First cancel the local share in this room if any. + if (update.roomId in localSharingRoomIds.value) { + stopLocalShare(roomId = update.roomId) + } + syncedActiveShareIds.update { + if (update.isLive) { + it + update.beaconId + } else { + it - update.beaconId + } + } + } + .launchIn(matrixClient.sessionCoroutineScope) + + sessionObserver.addListener(sessionListener) + } + } + + private val sessionListener: SessionListener = object : SessionListener { + override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) { + if (matrixClient.sessionId.value == userId) { + clear() + } + } + } + + override suspend fun startShare(roomId: RoomId, duration: Duration): Result = withContext(NonCancellable) { + Timber.d("ActiveLiveLocationShareManager starting share for room $roomId with duration ${duration.inWholeSeconds}s") + val room = cachedRooms.getOrPut(roomId) { + matrixClient.getJoinedRoom(roomId) ?: return@withContext Result.failure(IllegalStateException("No room found for $roomId")) + } + // Before starting a new location share, stop the current one if any is active. + room.stopLiveLocationShare() + + room.startLiveLocationShare(duration.inWholeMilliseconds) + .onSuccess { beaconId -> + Timber.d("ActiveLiveLocationShareManager wait remote echo of $beaconId") + syncedActiveShareIds.first { beaconIds -> beaconIds.contains(beaconId) } + val expiresAt = Instant.fromEpochMilliseconds(clock.epochMillis() + duration.inWholeMilliseconds) + startLocalShare(roomId, expiresAt) + } + .onFailure { + Timber.e(it, "ActiveLiveLocationShareManager failed to start share for room $roomId") + stopLocalShare(roomId) + } + .map { } + } + + override suspend fun stopShare(roomId: RoomId): Result = withContext(NonCancellable) { + Timber.d("ActiveLiveLocationShareManager stopping share for room $roomId") + val room = cachedRooms.getOrPut(roomId) { + matrixClient.getJoinedRoom(roomId) ?: return@withContext Result.failure(IllegalStateException("No room found for $roomId")) + } + room.stopLiveLocationShare() + .onSuccess { + Timber.d("ActiveLiveLocationShareManager share stopped successfully for room $roomId") + } + .onFailure { + Timber.e(it, "ActiveLiveLocationShareManager failed to stop share for room $roomId") + } + .also { + stopLocalShare(roomId) + } + } + + override suspend fun onUnrecoverableError() { + Timber.d("ActiveLiveLocationShareManager unrecoverable error, stopping all shares") + localSharingRoomIds.value.toList().forEach { stopShare(it) } + } + + override suspend fun onLocationUpdate(location: Location) { + val activeSharesCount = localSharingRoomIds.value.size + Timber.d("ActiveLiveLocationShareManager received location update for $activeSharesCount active share(s)") + localSharingRoomIds.value.forEach { roomId -> + Timber.d("ActiveLiveLocationShareManager sending location to room $roomId") + sendLiveLocation(roomId, location) + .onFailure { + Timber.e(it, "ActiveLiveLocationShareManager failed to send location to room $roomId") + } + } + } + + private suspend fun sendLiveLocation(roomId: RoomId, location: Location): Result { + val room = cachedRooms.getOrPut(roomId) { + matrixClient.getJoinedRoom(roomId) ?: return Result.failure(IllegalStateException("No room found for $roomId")) + } + return room.sendLiveLocation(location.toGeoUri()) + .recoverCatching { exception -> + when (exception) { + is LiveLocationException.NotLive -> { + stopLocalShare(roomId) + throw exception + } + else -> throw exception + } + } + } + + private suspend fun startLocalShare(roomId: RoomId, expiresAt: Instant) { + val wasEmpty = localSharingRoomIds.value.isEmpty() + Timber.d("ActiveLiveLocationShareManager share started successfully for room $roomId (wasEmpty=$wasEmpty)") + localSharingRoomIds.update { it + roomId } + liveLocationStore.setLiveLocationExpiry(roomId, expiresAt) + scheduleTimeout(roomId, expiresAt) + if (wasEmpty) { + Timber.d("ActiveLiveLocationShareManager registering with coordinator for session ${matrixClient.sessionId}") + coordinator.register(matrixClient.sessionId, this@DefaultActiveLiveLocationShareManager) + } + } + + private suspend fun recoverPersistedShares() { + val now = Instant.fromEpochMilliseconds(clock.epochMillis()) + liveLocationStore.getLiveLocationExpiries().forEach { (roomId, expiresAt) -> + if (expiresAt > now) { + // Only starts locally as the share is already started remotely + startLocalShare(roomId, expiresAt) + } else { + // Explicitly stop the share on the server. + stopShare(roomId) + } + } + } + + private fun scheduleTimeout(roomId: RoomId, expiresAt: Instant) { + timeoutJobs.remove(roomId)?.cancel() + val delayMillis = expiresAt.toEpochMilliseconds() - clock.epochMillis() + timeoutJobs[roomId] = matrixClient.sessionCoroutineScope.launch { + delay(delayMillis) + stopShare(roomId) + .onFailure { error -> + Timber.e(error, "ActiveLiveLocationShareManager failed to stop timed out share for room $roomId") + } + } + } + + private suspend fun stopLocalShare(roomId: RoomId) { + Timber.d("ActiveLiveLocationShareManager stop local share in $roomId") + timeoutJobs.remove(roomId)?.cancel() + val wasSharing = localSharingRoomIds.getAndUpdate { it - roomId }.isNotEmpty() + cachedRooms.remove(roomId)?.close() + liveLocationStore.removeLiveLocationExpiry(roomId) + if (wasSharing && localSharingRoomIds.value.isEmpty()) { + Timber.d("ActiveLiveLocationShareManager unregistering from coordinator for session ${matrixClient.sessionId}") + coordinator.unregister(matrixClient.sessionId) + } + } + + private suspend fun clear() { + Timber.d("ActiveLiveLocationShareManager clear state") + sessionObserver.removeListener(sessionListener) + coordinator.unregister(matrixClient.sessionId) + liveLocationStore.clear() + for (room in cachedRooms.values) { + room.close() + timeoutJobs[room.roomId]?.cancel() + } + timeoutJobs.clear() + cachedRooms.clear() + localSharingRoomIds.value = emptySet() + syncedActiveShareIds.value = emptySet() + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/LiveLocationStore.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/LiveLocationStore.kt new file mode 100644 index 0000000000..417d9d423a --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/LiveLocationStore.kt @@ -0,0 +1,94 @@ +/* + * 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.location.impl.live + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringSetPreferencesKey +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.androidutils.hash.hash +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory +import kotlinx.coroutines.flow.first +import timber.log.Timber +import kotlin.time.Instant + +private const val LIVE_LOCATION_EXPIRY_VALUE_SEPARATOR = "=" + +@Inject +@SingleIn(SessionScope::class) +class LiveLocationStore( + preferenceDataStoreFactory: PreferenceDataStoreFactory, + sessionId: SessionId, +) { + private val store = preferenceDataStoreFactory.create("location_${sessionId.value.hash().take(16)}") + private val acceptedLiveLocationDisclaimerKey = booleanPreferencesKey("live_location_disclaimer_accepted") + private val liveLocationExpiriesKey = stringSetPreferencesKey("live_location_expiries") + + suspend fun hasAcceptedLiveLocationDisclaimer(): Boolean = runCatchingExceptions { + store.data.first()[acceptedLiveLocationDisclaimerKey] ?: false + }.getOrDefault(false) + + suspend fun setAcceptedLiveLocationDisclaimer(): Result = runCatchingExceptions { + store.edit { prefs -> + prefs[acceptedLiveLocationDisclaimerKey] = true + } + } + + suspend fun getLiveLocationExpiries(): Map = runCatchingExceptions { + val serialized = store.data.first()[liveLocationExpiriesKey].orEmpty() + decodeLiveLocationExpiries(serialized) + }.onFailure { error -> + Timber.e(error, "Failed to decode live location expiry payload") + }.getOrDefault(emptyMap()) + + suspend fun setLiveLocationExpiry(roomId: RoomId, expiresAt: Instant): Result = runCatchingExceptions { + store.edit { prefs -> + val current = decodeLiveLocationExpiries(prefs[liveLocationExpiriesKey].orEmpty()) + prefs[liveLocationExpiriesKey] = encodeLiveLocationExpiries(current + (roomId to expiresAt)) + } + } + + suspend fun removeLiveLocationExpiry(roomId: RoomId): Result = runCatchingExceptions { + store.edit { prefs -> + val current = decodeLiveLocationExpiries(prefs[liveLocationExpiriesKey].orEmpty()) + val updated = current - roomId + if (updated.isEmpty()) { + prefs.remove(liveLocationExpiriesKey) + } else { + prefs[liveLocationExpiriesKey] = encodeLiveLocationExpiries(updated) + } + } + } + + private fun decodeLiveLocationExpiries(serialized: Set): Map { + return runCatchingExceptions { + serialized + .map { it.split(LIVE_LOCATION_EXPIRY_VALUE_SEPARATOR) } + .associate { values -> + val roomId = RoomId(values[0]) + val expiresAtMillis = values[1].toLong() + roomId to Instant.fromEpochMilliseconds(expiresAtMillis) + } + }.getOrDefault(emptyMap()) + } + + private fun encodeLiveLocationExpiries(expiries: Map): Set { + return expiries.entries.map { (roomId, expiresAt) -> + "${roomId.value}$LIVE_LOCATION_EXPIRY_VALUE_SEPARATOR${expiresAt.toEpochMilliseconds()}" + }.toSet() + } + + suspend fun clear() { + store.edit { prefs -> prefs.clear() } + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/notification/LiveLocationSharingNotificationCreator.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/notification/LiveLocationSharingNotificationCreator.kt new file mode 100644 index 0000000000..9d4c461b0e --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/notification/LiveLocationSharingNotificationCreator.kt @@ -0,0 +1,61 @@ +/* + * 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.location.impl.live.notification + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.ui.strings.CommonStrings + +@Inject +class LiveLocationSharingNotificationCreator( + @ApplicationContext private val context: Context, + private val buildMeta: BuildMeta, +) { + companion object { + const val CHANNEL_ID = "LIVE_LOCATION_SHARING" + } + + fun createNotification(): Notification { + if (supportNotificationChannels()) { + ensureChannelExists() + } + return NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_menu_mylocation) + .setContentTitle(context.getString(CommonStrings.live_location_sharing_foreground_service_title_android, buildMeta.applicationName)) + .setContentText(context.getString(CommonStrings.live_location_sharing_foreground_service_message_android)) + .setOngoing(true) + .build() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun ensureChannelExists() { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(CommonStrings.live_location_sharing_foreground_service_channel_title_android) + .ifEmpty { "Live Location Sharing" }, + NotificationManager.IMPORTANCE_LOW, + ) + ) + } + } + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) + private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationReceiver.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationReceiver.kt new file mode 100644 index 0000000000..697a074093 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationReceiver.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.features.location.impl.live.service + +import io.element.android.features.location.api.Location + +interface LiveLocationReceiver { + suspend fun onLocationUpdate(location: Location) + suspend fun onUnrecoverableError() +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingCoordinator.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingCoordinator.kt new file mode 100644 index 0000000000..63618baa3c --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingCoordinator.kt @@ -0,0 +1,109 @@ +/* + * 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.location.impl.live.service + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.features.location.api.Location +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.services.toolbox.api.systemclock.SystemClock +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.atomics.AtomicLong +import kotlin.concurrent.atomics.AtomicReference +import kotlin.concurrent.atomics.ExperimentalAtomicApi +import kotlin.time.Duration.Companion.seconds + +private val THROTTLE_WINDOW = 3.seconds + +@OptIn(ExperimentalAtomicApi::class) +@SingleIn(AppScope::class) +class LiveLocationSharingCoordinator internal constructor( + private val startService: () -> Unit, + private val stopService: () -> Unit, + private val nowMillis: () -> Long, +) { + @Inject + constructor(@ApplicationContext context: Context, clock: SystemClock) : this( + startService = { + ContextCompat.startForegroundService(context, Intent(context, LiveLocationSharingService::class.java)) + }, + stopService = { + context.stopService(Intent(context, LiveLocationSharingService::class.java)) + }, + nowMillis = clock::epochMillis + ) + + private val receivers = ConcurrentHashMap() + + private val lastDispatchMillis = AtomicLong(0L) + private val lastKnownLocation = AtomicReference(null) + + suspend fun register(sessionId: SessionId, receiver: LiveLocationReceiver) { + val wasEmpty = receivers.isEmpty() + Timber.d("LiveLocationSharingCoordinator registering receiver for session $sessionId (wasEmpty=$wasEmpty)") + receivers[sessionId] = receiver + if (wasEmpty) { + Timber.d("LiveLocationSharingCoordinator starting service") + runCatchingExceptions(startService).onFailure { + Timber.e(it, "Failed to start live location sharing service") + } + } + lastKnownLocation.load()?.let { + dispatch(it) + } + } + + fun unregister(sessionId: SessionId) { + Timber.d("LiveLocationSharingCoordinator unregistering receiver for session $sessionId") + receivers.remove(sessionId) + if (receivers.isEmpty()) { + lastKnownLocation.store(null) + Timber.d("LiveLocationSharingCoordinator stopping service (no more receivers)") + runCatchingExceptions(stopService).onFailure { + Timber.e(it, "Failed to stop live location sharing service") + } + } + } + + suspend fun dispatchUnrecoverableError() { + Timber.d("LiveLocationSharingCoordinator dispatching unrecoverable error") + receivers.forEach { (sessionId, receiver) -> + runCatchingExceptions { + receiver.onUnrecoverableError() + }.onFailure { + Timber.e(it, "Failed to dispatch unrecoverable error for session $sessionId") + } + } + } + + suspend fun dispatch(location: Location) { + val currentTimeMillis = nowMillis() + val millisSincePrevious = currentTimeMillis - lastDispatchMillis.load() + if (millisSincePrevious < THROTTLE_WINDOW.inWholeMilliseconds) { + Timber.d("Received location before $THROTTLE_WINDOW, ignore.") + return + } + lastKnownLocation.store(location) + lastDispatchMillis.store(currentTimeMillis) + receivers.forEach { (sessionId, receiver) -> + Timber.d("Dispatch received location for session $sessionId ") + runCatchingExceptions { + receiver.onLocationUpdate(location) + }.onFailure { + Timber.e(it, "Failed to dispatch live location update for session $sessionId") + } + } + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt new file mode 100644 index 0000000000..21a0a081d8 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt @@ -0,0 +1,135 @@ +/* + * 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.location.impl.live.service + +import android.annotation.SuppressLint +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION +import android.os.IBinder +import androidx.core.app.ServiceCompat +import dev.zacsweers.metro.Inject +import io.element.android.features.location.impl.common.userlocation.PlatformLocationProvider +import io.element.android.features.location.impl.di.LocationBindings +import io.element.android.features.location.impl.live.notification.LiveLocationSharingNotificationCreator +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.di.annotations.AppCoroutineScope +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.push.api.notifications.ForegroundServiceType +import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.maplibre.compose.location.DesiredAccuracy +import org.maplibre.compose.location.PermissionException +import org.maplibre.spatialk.units.extensions.inMeters +import org.maplibre.spatialk.units.extensions.meters +import timber.log.Timber +import kotlin.time.Duration.Companion.seconds +import io.element.android.features.location.api.Location as ApiLocation + +private const val UPDATE_INTERVAL_IN_SECOND = 10 + +class LiveLocationSharingService : Service() { + @Inject lateinit var coordinator: LiveLocationSharingCoordinator + @Inject lateinit var notificationCreator: LiveLocationSharingNotificationCreator + @Inject lateinit var appPreferencesStore: AppPreferencesStore + @Inject lateinit var appForegroundStateService: AppForegroundStateService + + @AppCoroutineScope + @Inject lateinit var appCoroutineScope: CoroutineScope + private lateinit var coroutineScope: CoroutineScope + + override fun onBind(p0: Intent?): IBinder? = null + + @OptIn(FlowPreview::class) + @SuppressLint("InlinedApi") + override fun onCreate() { + super.onCreate() + Timber.d("LiveLocationSharingService onCreate") + bindings().inject(this) + runCatchingExceptions { + appForegroundStateService.updateIsSharingLiveLocation(true) + coroutineScope = appCoroutineScope.childScope(Dispatchers.Default, "LiveLocationSharingService") + val notificationId = NotificationIdProvider.getForegroundServiceNotificationId(ForegroundServiceType.LIVE_LOCATION) + Timber.d("LiveLocationSharingService starting foreground service with notificationId=$notificationId") + ServiceCompat.startForeground( + // service = + this, + // id = + notificationId, + // notification = + notificationCreator.createNotification(), + // foregroundServiceType = + FOREGROUND_SERVICE_TYPE_LOCATION, + ) + startLocationUpdatesListener() + }.onFailure { + Timber.e(it, "Failed to start live location sharing service") + appCoroutineScope.launch { coordinator.dispatchUnrecoverableError() } + stopSelf() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun startLocationUpdatesListener() { + Timber.d("LiveLocationSharingService listening to location updates") + appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow() + .flatMapLatest { minDistanceMeters -> + try { + PlatformLocationProvider( + context = applicationContext, + updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds, + minDistance = minDistanceMeters.meters, + desiredAccuracy = DesiredAccuracy.Balanced, + coroutineScope = coroutineScope + ).location + } catch (exception: PermissionException) { + Timber.e(exception, "Failed to create PlatformLocationProvider") + coordinator.dispatchUnrecoverableError() + emptyFlow() + } + } + .filterNotNull() + .map { location -> + ApiLocation( + lat = location.position.value.latitude, + lon = location.position.value.longitude, + accuracy = location.position.accuracy?.inMeters?.toFloat(), + ) + } + .onEach(coordinator::dispatch) + .launchIn(coroutineScope) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Timber.d("LiveLocationSharingService onStartCommand startId=$startId") + return START_STICKY + } + + override fun onDestroy() { + Timber.d("LiveLocationSharingService onDestroy") + if (::coroutineScope.isInitialized) { + coroutineScope.cancel() + } + appForegroundStateService.updateIsSharingLiveLocation(false) + super.onDestroy() + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt index d9ebc8b5af..e560ce805f 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt @@ -17,7 +17,8 @@ sealed interface ShareLocationEvent { val isPinned: Boolean, ) : ShareLocationEvent - data object ShowLiveLocationDurationPicker : ShareLocationEvent + data object InitiateLiveLocationShare : ShareLocationEvent + data object AcceptLiveLocationDisclaimer : ShareLocationEvent data class StartLiveLocationShare(val duration: Duration) : ShareLocationEvent data object StartTrackingUserLocation : ShareLocationEvent diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt index 10fddf1e50..a3f3cac432 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt @@ -14,6 +14,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.setValue @@ -21,26 +22,32 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Composer +import io.element.android.features.location.api.live.ActiveLiveLocationShareManager import io.element.android.features.location.impl.common.LocationConstraintsCheck import io.element.android.features.location.impl.common.MapDefaults +import io.element.android.features.location.impl.common.SendLiveLocationPermissions import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.location.impl.common.checkLocationConstraints import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsState +import io.element.android.features.location.impl.common.sendLiveLocationPermissions import io.element.android.features.location.impl.common.toDialogState -import io.element.android.features.location.impl.share.ShareLocationState.Dialog.Constraints +import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.features.location.impl.live.LiveLocationStore import io.element.android.features.messages.api.MessageComposerContext +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.runUpdatingState import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.dateformatter.api.DurationFormatter -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.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService @@ -60,9 +67,11 @@ class ShareLocationPresenter( private val messageComposerContext: MessageComposerContext, private val locationActions: LocationActions, private val buildMeta: BuildMeta, - private val featureFlagService: FeatureFlagService, private val client: MatrixClient, private val durationFormatter: DurationFormatter, + private val liveLocationShareManager: ActiveLiveLocationShareManager, + private val liveLocationStore: LiveLocationStore, + private val userLocationStateFactory: UserLocationState.Factory, ) : Presenter { @AssistedFactory fun interface Factory { @@ -75,23 +84,73 @@ class ShareLocationPresenter( override fun present(): ShareLocationState { val permissionsState: PermissionsState = permissionsPresenter.present() var trackUserPosition: Boolean by remember { mutableStateOf(permissionsState.isAnyGranted && locationActions.isLocationEnabled()) } - val isLiveLocationSharingEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing) - }.collectAsState(false) val appName by remember { derivedStateOf { buildMeta.applicationName } } var dialogState: ShareLocationState.Dialog by remember { mutableStateOf(ShareLocationState.Dialog.None) } + // true when trying to initiate the live location share + var pendingLiveLocationShare by remember { mutableStateOf(false) } + val startLiveLocationAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val currentUser by client.userProfile.collectAsState() + val customMapStyleUrl by produceState(AsyncData.Loading()) { + // Ignore errors + value = AsyncData.Success(client.getMapStyleUrl().getOrNull()) + } + val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms -> + perms.sendLiveLocationPermissions() + } val scope = rememberCoroutineScope() fun checkLocationConstraints() { - val locationConstraints = checkLocationConstraints(permissionsState, locationActions) - dialogState = Constraints(locationConstraints.toDialogState()) + val locationConstraints = checkLocationConstraints( + permissionsState = permissionsState, + locationActions = locationActions, + // No need to check SendLiveLocationPermissions here + sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED + ) + if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) { + permissionsState.eventSink(PermissionsEvents.RequestPermissions) + } trackUserPosition = locationConstraints is LocationConstraintsCheck.Success + dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) } - LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() } + suspend fun checkLiveLocationConstraints() { + val locationConstraints = checkLocationConstraints( + permissionsState = permissionsState, + locationActions = locationActions, + sendLiveLocationPermissions = sendLiveLocationPermissions, + ) + when (locationConstraints) { + LocationConstraintsCheck.Success -> { + val hasAcceptedDisclaimer = liveLocationStore.hasAcceptedLiveLocationDisclaimer() + dialogState = if (!hasAcceptedDisclaimer) { + ShareLocationState.Dialog.LiveLocationDisclaimer + } else { + val durations = LIVE_LOCATION_DURATIONS.map { + LiveLocationDuration(duration = it, formatted = durationFormatter.format(it)) + } + ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList()) + } + } + else -> { + if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) { + permissionsState.eventSink(PermissionsEvents.RequestPermissions) + } + dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) + } + } + } + + val userLocationState = userLocationStateFactory.create(permissionsState.isAnyGranted) + + LaunchedEffect(permissionsState) { + if (pendingLiveLocationShare) { + checkLiveLocationConstraints() + } else { + checkLocationConstraints() + } + } fun handleEvent(event: ShareLocationEvent) { when (event) { @@ -100,7 +159,10 @@ class ShareLocationPresenter( } ShareLocationEvent.StartTrackingUserLocation -> checkLocationConstraints() ShareLocationEvent.StopTrackingUserLocation -> trackUserPosition = false - ShareLocationEvent.DismissDialog -> dialogState = ShareLocationState.Dialog.None + ShareLocationEvent.DismissDialog -> { + pendingLiveLocationShare = false + dialogState = ShareLocationState.Dialog.None + } ShareLocationEvent.OpenAppSettings -> { locationActions.openAppSettings() dialogState = ShareLocationState.Dialog.None @@ -109,20 +171,25 @@ class ShareLocationPresenter( locationActions.openLocationSettings() dialogState = ShareLocationState.Dialog.None } - ShareLocationEvent.ShowLiveLocationDurationPicker -> { - val constraintsResult = checkLocationConstraints(permissionsState, locationActions) - dialogState = if (constraintsResult is LocationConstraintsCheck.Success) { - val durations = LIVE_LOCATION_DURATIONS.map { - LiveLocationDuration(duration = it, formatted = durationFormatter.format(it)) + ShareLocationEvent.InitiateLiveLocationShare -> scope.launch { + pendingLiveLocationShare = true + checkLiveLocationConstraints() + } + ShareLocationEvent.AcceptLiveLocationDisclaimer -> scope.launch { + liveLocationStore.setAcceptedLiveLocationDisclaimer() + .onSuccess { + checkLiveLocationConstraints() } - ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList()) - } else { - Constraints(constraintsResult.toDialogState()) - } } is ShareLocationEvent.StartLiveLocationShare -> scope.launch { + pendingLiveLocationShare = false dialogState = ShareLocationState.Dialog.None - // room.startLiveLocationShare(event.duration.inWholeMilliseconds) + startLiveLocationAction.runUpdatingState { + liveLocationShareManager.startShare( + roomId = room.roomId, + duration = event.duration, + ) + } } ShareLocationEvent.RequestPermissions -> { dialogState = ShareLocationState.Dialog.None @@ -132,12 +199,14 @@ class ShareLocationPresenter( } return ShareLocationState( + customMapStyleUrl = customMapStyleUrl, currentUser = currentUser, dialogState = dialogState, trackUserLocation = trackUserPosition, - hasLocationPermission = permissionsState.isAnyGranted, - canShareLiveLocation = isLiveLocationSharingEnabled, + userLocationState = userLocationState, + canShareLiveLocation = timelineMode.canShareLiveLocation(), appName = appName, + startLiveLocationAction = startLiveLocationAction.value, eventSink = ::handleEvent, ) } @@ -174,4 +243,9 @@ class ShareLocationPresenter( } } +private fun Timeline.Mode.canShareLiveLocation() = when (this) { + is Timeline.Mode.Thread -> false + else -> true +} + private fun generateBody(uri: String): String = "Location was shared at $uri" diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt index 8b1f494f1e..fb1b803435 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt @@ -9,21 +9,27 @@ package io.element.android.features.location.impl.share import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState +import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList data class ShareLocationState( + val customMapStyleUrl: AsyncData, val currentUser: MatrixUser, val dialogState: Dialog, val trackUserLocation: Boolean, - val hasLocationPermission: Boolean, + val userLocationState: UserLocationState, val appName: String, val canShareLiveLocation: Boolean, + val startLiveLocationAction: AsyncAction, val eventSink: (ShareLocationEvent) -> Unit, ) { sealed interface Dialog { data object None : Dialog data class Constraints(val state: LocationConstraintsDialogState) : Dialog + data object LiveLocationDisclaimer : Dialog data class LiveLocationDurations(val durations: ImmutableList) : Dialog } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt index facef74346..ed8d46afd3 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt @@ -10,6 +10,9 @@ package io.element.android.features.location.impl.share import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState +import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.persistentListOf @@ -24,32 +27,36 @@ class ShareLocationStateProvider : PreviewParameterProvider aShareLocationState( dialogState = ShareLocationState.Dialog.None, trackUserPosition = false, - hasLocationPermission = false, ), aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), trackUserPosition = false, - hasLocationPermission = false, ), aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), trackUserPosition = false, - hasLocationPermission = false, ), aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), trackUserPosition = false, - hasLocationPermission = true, ), aShareLocationState( dialogState = ShareLocationState.Dialog.None, trackUserPosition = false, - hasLocationPermission = true, ), aShareLocationState( dialogState = ShareLocationState.Dialog.None, trackUserPosition = true, - hasLocationPermission = true, + ), + aShareLocationState( + dialogState = ShareLocationState.Dialog.None, + trackUserPosition = true, + canShareLiveLocation = true, + ), + aShareLocationState( + dialogState = ShareLocationState.Dialog.LiveLocationDisclaimer, + trackUserPosition = true, + canShareLiveLocation = true, ), aShareLocationState( dialogState = ShareLocationState.Dialog.LiveLocationDurations( @@ -60,28 +67,34 @@ class ShareLocationStateProvider : PreviewParameterProvider ) ), trackUserPosition = true, - hasLocationPermission = true, canShareLiveLocation = true, ), + aShareLocationState( + customMapStyleUrl = AsyncData.Loading(), + ), ) } fun aShareLocationState( + customMapStyleUrl: AsyncData = AsyncData.Success(null), currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")), dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None, trackUserPosition: Boolean = false, - hasLocationPermission: Boolean = false, + userLocationState: UserLocationState = UserLocationState(null), canShareLiveLocation: Boolean = false, appName: String = APP_NAME, + startLiveLocationAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (ShareLocationEvent) -> Unit = {}, ): ShareLocationState { return ShareLocationState( + customMapStyleUrl = customMapStyleUrl, currentUser = currentUser, dialogState = dialogState, trackUserLocation = trackUserPosition, - hasLocationPermission = hasLocationPermission, + userLocationState = userLocationState, canShareLiveLocation = canShareLiveLocation, appName = appName, + startLiveLocationAction = startLiveLocationAction, eventSink = eventSink ) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 1e163f417d..4760b3597c 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -26,10 +26,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -43,12 +43,17 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold import io.element.android.features.location.impl.common.ui.UserLocationPuck -import io.element.android.features.location.impl.common.ui.rememberUserLocationState -import io.element.android.libraries.androidutils.system.toast +import io.element.android.features.location.impl.common.userlocation.UserLocationTrackingEffect +import io.element.android.features.location.impl.share.ShareLocationEvent.StartLiveLocationShare +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.LocationPin import io.element.android.libraries.designsystem.components.PinVariant +import io.element.android.libraries.designsystem.components.async.AsyncIndicator +import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost +import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ListDialog import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.RadioButtonListItem @@ -64,7 +69,6 @@ import kotlinx.collections.immutable.ImmutableList import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.rememberCameraState -import org.maplibre.compose.location.UserLocationState import kotlin.time.Duration @OptIn(ExperimentalMaterial3Api::class) @@ -74,7 +78,6 @@ fun ShareLocationView( navigateUp: () -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current when (val dialogState = state.dialogState) { ShareLocationState.Dialog.None -> Unit is ShareLocationState.Dialog.Constraints -> LocationConstraintsDialog( @@ -85,12 +88,17 @@ fun ShareLocationView( onOpenLocationSettings = { state.eventSink(ShareLocationEvent.OpenLocationSettings) }, onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, ) + ShareLocationState.Dialog.LiveLocationDisclaimer -> ConfirmationDialog( + content = stringResource(R.string.screen_share_location_live_location_disclaimer_title), + submitText = stringResource(CommonStrings.action_accept), + cancelText = stringResource(CommonStrings.action_decline), + onSubmitClick = { state.eventSink(ShareLocationEvent.AcceptLiveLocationDisclaimer) }, + onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, + ) is ShareLocationState.Dialog.LiveLocationDurations -> LiveLocationDurationDialog( durations = dialogState.durations, onSelectDuration = { duration -> - state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration)) - context.toast("Not implemented yet!") - navigateUp() + state.eventSink(StartLiveLocationShare(duration)) }, onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, ) @@ -100,7 +108,6 @@ fun ShareLocationView( bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded) ) val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition) - val userLocationState = rememberUserLocationState(state.hasLocationPermission) LaunchedEffect(cameraState.isCameraMoving) { if (cameraState.moveReason == CameraMoveReason.GESTURE) { @@ -109,6 +116,7 @@ fun ShareLocationView( } MapBottomSheetScaffold( + customMapStyleUrl = state.customMapStyleUrl, cameraState = cameraState, modifier = modifier, scaffoldState = scaffoldState, @@ -126,15 +134,18 @@ fun ShareLocationView( BottomSheetContent( cameraState = cameraState, state = state, - userLocationState = userLocationState, navigateUp = navigateUp ) }, mapContent = { + UserLocationTrackingEffect( + cameraState = cameraState, + locationState = state.userLocationState, + enabled = state.trackUserLocation, + ) UserLocationPuck( cameraState = cameraState, - locationState = userLocationState, - trackUserLocation = state.trackUserLocation + location = state.userLocationState.location, ) }, overlayContent = { sheetPadding -> @@ -160,26 +171,62 @@ fun ShareLocationView( .align(Alignment.TopEnd) .padding(all = 16.dp), ) + StartLiveLocationActionView(state.startLiveLocationAction, navigateUp) } ) } +@Composable +private fun StartLiveLocationActionView( + action: AsyncAction, + onActionSuccess: () -> Unit, + modifier: Modifier = Modifier, +) { + val updatedOnActionSuccess by rememberUpdatedState(onActionSuccess) + Box(modifier = modifier) { + val asyncIndicatorState = rememberAsyncIndicatorState() + AsyncIndicatorHost(state = asyncIndicatorState) + + when (action) { + is AsyncAction.Loading -> { + LaunchedEffect(action) { + asyncIndicatorState.enqueue { + AsyncIndicator.Loading(text = stringResource(CommonStrings.common_waiting_live_location)) + } + } + } + is AsyncAction.Failure -> { + LaunchedEffect(action) { + asyncIndicatorState.enqueue(AsyncIndicator.DURATION_SHORT) { + AsyncIndicator.Failure( + text = stringResource(CommonStrings.common_something_went_wrong), + ) + } + } + } + is AsyncAction.Success -> { + LaunchedEffect(action) { updatedOnActionSuccess() } + } + else -> Unit + } + } +} + @Composable private fun BottomSheetContent( cameraState: CameraState, state: ShareLocationState, - userLocationState: UserLocationState, navigateUp: () -> Unit, ) { Spacer(Modifier.height(20.dp)) - val userLocation = userLocationState.location + val userLocation = state.userLocationState.location if (state.trackUserLocation && userLocation != null) { ShareCurrentLocationItem { state.eventSink( ShareLocationEvent.ShareStaticLocation( location = Location( - lat = userLocation.position.latitude, - lon = userLocation.position.longitude + lat = userLocation.position.value.latitude, + lon = userLocation.position.value.longitude ), isPinned = false ) @@ -202,7 +249,7 @@ private fun BottomSheetContent( } if (state.canShareLiveLocation) { ShareLiveLocationItem { - state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) + state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) } } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvent.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvent.kt index 6a3e3521e0..34132dccf3 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvent.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvent.kt @@ -17,4 +17,5 @@ sealed interface ShowLocationEvent { data object RequestPermissions : ShowLocationEvent data object OpenAppSettings : ShowLocationEvent data object OpenLocationSettings : ShowLocationEvent + data object StopLocationSharing : ShowLocationEvent } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index 43d3aa6d00..f187e91c53 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -10,19 +10,25 @@ package io.element.android.features.location.impl.show import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +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.rememberUpdatedState 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.location.api.Location import io.element.android.features.location.api.ShowLocationMode +import io.element.android.features.location.api.live.ActiveLiveLocationShareManager +import io.element.android.features.location.api.live.isCurrentlySharing import io.element.android.features.location.impl.common.LocationConstraintsCheck import io.element.android.features.location.impl.common.MapDefaults +import io.element.android.features.location.impl.common.SendLiveLocationPermissions import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.location.impl.common.checkLocationConstraints import io.element.android.features.location.impl.common.permissions.PermissionsEvents @@ -30,6 +36,9 @@ import io.element.android.features.location.impl.common.permissions.PermissionsP import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.toDialogState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState +import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.features.location.impl.common.userlocation.asMapLibreLocation +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.meta.BuildMeta @@ -37,6 +46,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.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.room.joinedRoomMembers @@ -45,19 +55,21 @@ import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch -@AssistedInject -class ShowLocationPresenter( +@AssistedInject class ShowLocationPresenter( @Assisted private val mode: ShowLocationMode, permissionsPresenterFactory: PermissionsPresenter.Factory, private val locationActions: LocationActions, private val buildMeta: BuildMeta, private val dateFormatter: DateFormatter, private val stringProvider: StringProvider, + private val client: MatrixClient, private val joinedRoom: JoinedRoom, + private val liveLocationShareManager: ActiveLiveLocationShareManager, + private val userLocationStateFactory: UserLocationState.Factory, ) : Presenter { - @AssistedFactory - fun interface Factory { + @AssistedFactory fun interface Factory { fun create(mode: ShowLocationMode): ShowLocationPresenter } @@ -65,6 +77,7 @@ class ShowLocationPresenter( @Composable override fun present(): ShowLocationState { + val coroutineScope = rememberCoroutineScope() val permissionsState: PermissionsState = permissionsPresenter.present() var isTrackMyLocation by remember { mutableStateOf(false) } val appName by remember { derivedStateOf { buildMeta.applicationName } } @@ -72,10 +85,27 @@ class ShowLocationPresenter( mutableStateOf(LocationConstraintsDialogState.None) } - LaunchedEffect(permissionsState.permissions) { - if (permissionsState.isAnyGranted) { - dialogState = LocationConstraintsDialogState.None + val customMapStyleUrl by produceState(AsyncData.Loading()) { + // Ignore errors + value = AsyncData.Success(client.getMapStyleUrl().getOrNull()) + } + + fun checkLocationConstraints() { + val locationConstraints = checkLocationConstraints( + permissionsState = permissionsState, + locationActions = locationActions, + // No need to check SendLiveLocationPermissions here + sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED + ) + if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) { + permissionsState.eventSink(PermissionsEvents.RequestPermissions) } + isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success + dialogState = locationConstraints.toDialogState() + } + + LaunchedEffect(permissionsState) { + checkLocationConstraints() } fun handleEvent(event: ShowLocationEvent) { @@ -85,9 +115,7 @@ class ShowLocationPresenter( } is ShowLocationEvent.TrackMyLocation -> { if (event.enabled) { - val locationConstraints = checkLocationConstraints(permissionsState, locationActions) - isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success - dialogState = locationConstraints.toDialogState() + checkLocationConstraints() } else { isTrackMyLocation = false } @@ -102,6 +130,9 @@ class ShowLocationPresenter( dialogState = LocationConstraintsDialogState.None } ShowLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) + ShowLocationEvent.StopLocationSharing -> coroutineScope.launch { + liveLocationShareManager.stopShare(joinedRoom.roomId) + } } } @@ -127,6 +158,7 @@ class ShowLocationPresenter( location = mode.location, isLive = false, assetType = mode.assetType, + isOwnUser = mode.senderId == joinedRoom.sessionId ) ) } @@ -137,53 +169,66 @@ class ShowLocationPresenter( val liveLocationSharesFlow = joinedRoom.subscribeToLiveLocationShares() val membersStateFlow = joinedRoom.membersStateFlow.mapState { it.joinedRoomMembers() } combine(liveLocationSharesFlow, membersStateFlow) { liveShares, members -> - liveShares - .sortedWith(comparator) - .mapNotNull { share -> - val lastLocation = share.lastLocation ?: return@mapNotNull null - val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null - val member = members.find { it.userId == share.userId } - val displayName = member?.getBestName() ?: share.userId.value - val avatarUrl = member?.avatarUrl - val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true) - val formattedTimestamp = stringProvider.getString( - CommonStrings.screen_static_location_sheet_timestamp_description, - relativeTime - ) - LocationShareItem( - userId = share.userId, - displayName = displayName, - avatarData = AvatarData( - id = share.userId.value, - name = displayName, - url = avatarUrl, - size = AvatarSize.UserListItem, - ), - formattedTimestamp = formattedTimestamp, - location = location, - isLive = true, - assetType = lastLocation.assetType, - ) - } - .toImmutableList() + liveShares.sortedWith(comparator).mapNotNull { share -> + val lastLocation = share.lastLocation ?: return@mapNotNull null + val location = Location.fromGeoUri(lastLocation.geoUri) ?: return@mapNotNull null + val member = members.find { it.userId == share.userId } + val displayName = member?.getBestName() ?: share.userId.value + val avatarUrl = member?.avatarUrl + val relativeTime = dateFormatter.format(timestamp = lastLocation.timestamp, mode = DateFormatterMode.Full, useRelative = true) + val formattedTimestamp = stringProvider.getString( + CommonStrings.screen_static_location_sheet_timestamp_description, + relativeTime + ) + LocationShareItem( + userId = share.userId, + displayName = displayName, + avatarData = AvatarData( + id = share.userId.value, + name = displayName, + url = avatarUrl, + size = AvatarSize.UserListItem, + ), + formattedTimestamp = formattedTimestamp, + location = location, + isLive = true, + assetType = lastLocation.assetType, + isOwnUser = share.userId == joinedRoom.sessionId + ) + }.toImmutableList() }.collect { value = it } }.value } } - val focusedLocation = when (mode) { - is ShowLocationMode.Static -> locationShares.firstOrNull() - is ShowLocationMode.Live -> locationShares.firstOrNull { it.userId == mode.senderId } + val updatedLocationShares by rememberUpdatedState(locationShares) + val focusedLocation by remember { + derivedStateOf { + when (mode) { + is ShowLocationMode.Static -> updatedLocationShares.firstOrNull() + is ShowLocationMode.Live -> updatedLocationShares.firstOrNull { it.userId == mode.senderId } + } + } + } + val isCurrentlySharing by liveLocationShareManager.isCurrentlySharing(roomId = joinedRoom.roomId).collectAsState() + val hideUserLocationPuck = mode is ShowLocationMode.Live && isCurrentlySharing + val userLocationState = if (hideUserLocationPuck) { + // When sharing with this device, use the user LocationShareItem as source of data instead of the device. + val ownLocationShare by remember { derivedStateOf { updatedLocationShares.find { it.isOwnUser }?.location?.asMapLibreLocation() } } + UserLocationState(ownLocationShare) + } else { + userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted) } - return ShowLocationState( + customMapStyleUrl = customMapStyleUrl, dialogState = dialogState, locationShares = locationShares, focusedLocation = focusedLocation, - hasLocationPermission = permissionsState.isAnyGranted, isTrackMyLocation = isTrackMyLocation, + userLocationState = userLocationState, isLive = mode is ShowLocationMode.Live, appName = appName, + hideUserLocationPuck = hideUserLocationPuck, eventSink = ::handleEvent, ) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt index b6a60f35db..e5ddf589b0 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt @@ -11,6 +11,8 @@ package io.element.android.features.location.impl.show import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.ui.LocationMarkerData +import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId @@ -18,13 +20,15 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import kotlinx.collections.immutable.ImmutableList data class ShowLocationState( + val customMapStyleUrl: AsyncData, val isLive: Boolean, val dialogState: LocationConstraintsDialogState, val locationShares: ImmutableList, val focusedLocation: LocationShareItem?, - val hasLocationPermission: Boolean, val isTrackMyLocation: Boolean, + val userLocationState: UserLocationState, val appName: String, + val hideUserLocationPuck: Boolean, val eventSink: (ShowLocationEvent) -> Unit, ) { val isSheetDraggable = isLive && locationShares.isNotEmpty() @@ -38,7 +42,10 @@ data class LocationShareItem( val location: Location, val isLive: Boolean, val assetType: AssetType?, -) + val isOwnUser: Boolean +) { + val canStopSharing = isLive && isOwnUser +} fun LocationShareItem.toMarkerData(): LocationMarkerData { val pinVariant = if (assetType == AssetType.PIN) { diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt index 1ab2310365..cd7ea108bb 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt @@ -11,8 +11,11 @@ package io.element.android.features.location.impl.show import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState +import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.libraries.architecture.AsyncData 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.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.location.AssetType import kotlinx.collections.immutable.toImmutableList @@ -31,36 +34,34 @@ class ShowLocationStateProvider : PreviewParameterProvider { ), aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled, - hasLocationPermission = true, - ), - aShowLocationState( - hasLocationPermission = true, - ), - aShowLocationState( - hasLocationPermission = true, - isTrackMyLocation = true, ), + aShowLocationState(isTrackMyLocation = true), + aShowLocationState(customMapStyleUrl = AsyncData.Loading()), ) } private const val APP_NAME = "ApplicationName" fun aShowLocationState( + customMapStyleUrl: AsyncData = AsyncData.Success(null), isLive: Boolean = false, constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None, locationShares: List = listOf(aLocationShareItem(isLive = isLive)), focusedLocation: LocationShareItem? = locationShares.firstOrNull(), - hasLocationPermission: Boolean = false, isTrackMyLocation: Boolean = false, + userLocationState: UserLocationState = UserLocationState(null), appName: String = APP_NAME, + hideUserLocationPuck: Boolean = false, eventSink: (ShowLocationEvent) -> Unit = {}, ): ShowLocationState { return ShowLocationState( + customMapStyleUrl = customMapStyleUrl, dialogState = constraintsDialogState, locationShares = locationShares.toImmutableList(), focusedLocation = focusedLocation, - hasLocationPermission = hasLocationPermission, isTrackMyLocation = isTrackMyLocation, + userLocationState = userLocationState, + hideUserLocationPuck = hideUserLocationPuck, appName = appName, isLive = isLive, eventSink = eventSink, @@ -69,7 +70,7 @@ fun aShowLocationState( fun aLocationShareItem( userId: UserId = UserId("@alice:matrix.org"), - displayName: String = "Alice", + displayName: String = USER_NAME_ALICE, avatarData: AvatarData = AvatarData( id = userId.value, name = displayName, @@ -80,6 +81,7 @@ fun aLocationShareItem( assetType: AssetType? = null, formattedTimestamp: String = "Shared 1 min ago", location: Location = Location(1.23, 2.34, 4f), + isOwnUser: Boolean = false, ) = LocationShareItem( userId = userId, displayName = displayName, @@ -88,4 +90,5 @@ fun aLocationShareItem( location = location, isLive = isLive, assetType = assetType, + isOwnUser = isOwnUser, ) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 7ac5946723..6b1b33014d 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -43,13 +43,14 @@ import io.element.android.features.location.impl.common.ui.LocationPinMarkers import io.element.android.features.location.impl.common.ui.LocationShareRow import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold import io.element.android.features.location.impl.common.ui.UserLocationPuck -import io.element.android.features.location.impl.common.ui.rememberUserLocationState +import io.element.android.features.location.impl.common.userlocation.UserLocationTrackingEffect 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.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.camera.CameraPosition @@ -90,7 +91,6 @@ fun ShowLocationView( } } - val userLocationState = rememberUserLocationState(state.hasLocationPermission) val scaffoldState = rememberBottomSheetScaffoldState( bottomSheetState = rememberStandardBottomSheetState(SheetValue.Expanded) ) @@ -100,6 +100,7 @@ fun ShowLocationView( } } MapBottomSheetScaffold( + customMapStyleUrl = state.customMapStyleUrl, sheetDragHandle = if (state.isSheetDraggable) { { BottomSheetDefaults.DragHandle() } } else { @@ -147,12 +148,15 @@ fun ShowLocationView( LocationShareRow( item = locationShare, onShareClick = { state.eventSink(ShowLocationEvent.Share(locationShare.location)) }, + onStopClick = { state.eventSink(ShowLocationEvent.StopLocationSharing) }, modifier = Modifier.clickable { state.eventSink(ShowLocationEvent.TrackMyLocation(false)) val position = CameraPosition( padding = sheetPaddings, target = Position(locationShare.location.lon, locationShare.location.lat), - zoom = MapDefaults.DEFAULT_ZOOM + // Force pointing to NORTH + bearing = 0.0, + zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM), ) coroutineScope.launch { cameraState.animateTo(finalPosition = position) @@ -164,13 +168,19 @@ fun ShowLocationView( } }, mapContent = { - UserLocationPuck( + UserLocationTrackingEffect( cameraState = cameraState, - locationState = userLocationState, - trackUserLocation = state.isTrackMyLocation + locationState = state.userLocationState, + enabled = state.isTrackMyLocation, ) + if (!state.hideUserLocationPuck) { + UserLocationPuck( + cameraState = cameraState, + location = state.userLocationState.location, + ) + } val markers = remember(state.locationShares) { - state.locationShares.map { it.toMarkerData() } + state.locationShares.map { it.toMarkerData() }.toImmutableList() } LocationPinMarkers(markers) }, diff --git a/features/location/impl/src/main/res/values-cs/translations.xml b/features/location/impl/src/main/res/values-cs/translations.xml index 6bb5b1db8c..98af57abba 100644 --- a/features/location/impl/src/main/res/values-cs/translations.xml +++ b/features/location/impl/src/main/res/values-cs/translations.xml @@ -2,4 +2,5 @@ "Vaše historie aktuální polohy bude uložena v místnosti a bude viditelná pro členy i po skončení relace." "Zvolte, jak dlouho chcete sdílet svou aktuální polohu." + "Nemáte oprávnění sdílet svou aktuální polohu v této místnosti." diff --git a/features/location/impl/src/main/res/values-da/translations.xml b/features/location/impl/src/main/res/values-da/translations.xml index f15ab0fb2f..4d4c5d00bc 100644 --- a/features/location/impl/src/main/res/values-da/translations.xml +++ b/features/location/impl/src/main/res/values-da/translations.xml @@ -1,4 +1,5 @@ + "Din live-positionshistorik gemmes i rummet og er synlig for medlemmerne, når sessionen er afsluttet." "Vælg, hvor længe du vil dele din aktuelle position." diff --git a/features/location/impl/src/main/res/values-el/translations.xml b/features/location/impl/src/main/res/values-el/translations.xml new file mode 100644 index 0000000000..8ae7c58c74 --- /dev/null +++ b/features/location/impl/src/main/res/values-el/translations.xml @@ -0,0 +1,5 @@ + + + "Το ιστορικό ζωντανής τοποθεσίας σας θα αποθηκευτεί στην αίθουσα και θα είναι ορατό στα μέλη μετά το τέλος της συνεδρίας." + "Επιλέξτε για πόσο χρονικό διάστημα θα κοινοποιείτε την τρέχουσα τοποθεσία σας." + diff --git a/features/location/impl/src/main/res/values-et/translations.xml b/features/location/impl/src/main/res/values-et/translations.xml new file mode 100644 index 0000000000..73847bcb43 --- /dev/null +++ b/features/location/impl/src/main/res/values-et/translations.xml @@ -0,0 +1,6 @@ + + + "Sinu reaalajas jagatud asukoha ajalugu salvestub siin jututoas ja see on liikmetele nähtav ka pärast jagamissessiooni lõppu." + "Vali, kui kaua tahad oma reaalajas jagada." + "Sul pole õigust jagada selles jututoas oma asukohta reaalajas" + diff --git a/features/location/impl/src/main/res/values-fi/translations.xml b/features/location/impl/src/main/res/values-fi/translations.xml index 4a7d801f71..b35d11cd49 100644 --- a/features/location/impl/src/main/res/values-fi/translations.xml +++ b/features/location/impl/src/main/res/values-fi/translations.xml @@ -2,4 +2,5 @@ "Reaaliaikainen sijaintihistoriasi tallennetaan huoneeseen ja on jäsenten nähtävissä istunnon päätyttyä." "Valitse, kuinka kauan haluat jakaa reaaliaikaisen sijaintisi." + "Sinulla ei ole oikeuksia jakaa reaaliaikaista sijaintiasi tässä huoneessa" 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 043c180dbc..802b869271 100644 --- a/features/location/impl/src/main/res/values-fr/translations.xml +++ b/features/location/impl/src/main/res/values-fr/translations.xml @@ -2,4 +2,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." + "Vous n’êtes pas autorisé à partager votre position en direct dans ce salon." diff --git a/features/location/impl/src/main/res/values-hu/translations.xml b/features/location/impl/src/main/res/values-hu/translations.xml index b89965485f..0ccb2a2413 100644 --- a/features/location/impl/src/main/res/values-hu/translations.xml +++ b/features/location/impl/src/main/res/values-hu/translations.xml @@ -2,4 +2,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." + "Nincs jogosultsága az élő tartózkodási helyének megosztására ebben a szobában." diff --git a/features/location/impl/src/main/res/values-it/translations.xml b/features/location/impl/src/main/res/values-it/translations.xml index 235a9eba4a..44adc1e3c5 100644 --- a/features/location/impl/src/main/res/values-it/translations.xml +++ b/features/location/impl/src/main/res/values-it/translations.xml @@ -2,4 +2,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." + "Non hai l\'autorizzazione per condividere la tua posizione in tempo reale in questa stanza" diff --git a/features/location/impl/src/main/res/values-ja/translations.xml b/features/location/impl/src/main/res/values-ja/translations.xml index 971693ef11..1060120ee5 100644 --- a/features/location/impl/src/main/res/values-ja/translations.xml +++ b/features/location/impl/src/main/res/values-ja/translations.xml @@ -2,4 +2,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 index e283d20b9c..aa23450335 100644 --- a/features/location/impl/src/main/res/values-ko/translations.xml +++ b/features/location/impl/src/main/res/values-ko/translations.xml @@ -2,4 +2,5 @@ "실시간 위치 기록은 대화방에 저장되며, 공유 종료 후에도 멤버들이 확인할 수 있습니다." "실시간 위치를 공유할 시간을 선택해 주세요." + "이 방에서는 실시간 위치를 공유할 권한이 없습니다." diff --git a/features/location/impl/src/main/res/values-pl/translations.xml b/features/location/impl/src/main/res/values-pl/translations.xml new file mode 100644 index 0000000000..c480d0f43b --- /dev/null +++ b/features/location/impl/src/main/res/values-pl/translations.xml @@ -0,0 +1,6 @@ + + + "Twoja historia lokalizacji na żywo zostanie zapisana w pokoju i będzie widoczna dla członków po zakończeniu sesji." + "Wybierz, jak długo chcesz udostępniać swoją lokalizację na żywo." + "Nie masz uprawnień do udostępniania swojej lokalizacji na żywo w tym pokoju" + diff --git a/features/location/impl/src/main/res/values-ro/translations.xml b/features/location/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..85e665647a --- /dev/null +++ b/features/location/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,6 @@ + + + "Istoricul locațiilor dumneavoastră va fi stocat în cameră și va fi vizibil pentru membri după încheierea sesiunii." + "Alegeți cât timp doriți să vă partajați locația în timp real." + "Nu aveți permisiunea de a vă partaja locația în această cameră." + diff --git a/features/location/impl/src/main/res/values-ru/translations.xml b/features/location/impl/src/main/res/values-ru/translations.xml index 3c496f1d39..97b279621c 100644 --- a/features/location/impl/src/main/res/values-ru/translations.xml +++ b/features/location/impl/src/main/res/values-ru/translations.xml @@ -2,4 +2,5 @@ "История вашего местоположения в режиме реального времени будет сохранена в комнате и станет доступна участникам после окончания сессии." "Выберите, как долго вы будете делиться своим местоположением в режиме реального времени." + "У тебя нет прав на то, чтобы делиться своим текущим местоположением в этой комнате" diff --git a/features/location/impl/src/main/res/values-uk/translations.xml b/features/location/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..08d205d925 --- /dev/null +++ b/features/location/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,6 @@ + + + "Ваша історія поточного місцезнаходження зберігатиметься у кімнаті та буде доступна учасникам після завершення сеансу." + "Виберіть, як довго ділитися своїм місцезнаходженням." + "У вас немає дозволу ділитися своїм місцезнаходженням у реальному часі в цій кімнаті" + diff --git a/features/location/impl/src/main/res/values-zh/translations.xml b/features/location/impl/src/main/res/values-zh/translations.xml index 6837d3a1eb..86f2f2a9da 100644 --- a/features/location/impl/src/main/res/values-zh/translations.xml +++ b/features/location/impl/src/main/res/values-zh/translations.xml @@ -1,5 +1,6 @@ - "你实时位置历史将存储在房间中,并于会话结束后对其他成员可见。" + "你的实时位置历史将存储在房间中,并于会话结束后对其他成员可见。" "选择共享实时位置的时长。" + "你无权在此房内共享实时位置。" diff --git a/features/location/impl/src/main/res/values/localazy.xml b/features/location/impl/src/main/res/values/localazy.xml index ac2ff4b2a0..975bb3c6ea 100644 --- a/features/location/impl/src/main/res/values/localazy.xml +++ b/features/location/impl/src/main/res/values/localazy.xml @@ -2,4 +2,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." + "You do not have permissions to share your live location in this room" diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt index a4352444dc..5c7bacb91a 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/PermissionsStateFactory.kt @@ -13,10 +13,12 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS fun aPermissionsState( permissions: PermissionsState.Permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale: Boolean = false, + permissionsRequested: Boolean = false, ): PermissionsState { return PermissionsState( permissions = permissions, shouldShowRationale = shouldShowRationale, + permissionsAlreadyRequested = permissionsRequested, eventSink = {}, ) } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/FakeUserLocationStateFactory.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/FakeUserLocationStateFactory.kt new file mode 100644 index 0000000000..f6855c5667 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/FakeUserLocationStateFactory.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.location.impl.common + +import androidx.compose.runtime.Composable +import io.element.android.features.location.impl.common.userlocation.UserLocationState + +class FakeUserLocationStateFactory : UserLocationState.Factory { + @Composable + override fun create(hasLocationPermission: Boolean): UserLocationState { + return UserLocationState(null) + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt index c8e1f21a48..40362bda3c 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt @@ -21,7 +21,7 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = true) - val result = checkLocationConstraints(permissionsState, locationActions) + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) assertThat(result).isEqualTo(LocationConstraintsCheck.Success) } @@ -33,7 +33,7 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = true) - val result = checkLocationConstraints(permissionsState, locationActions) + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) assertThat(result).isEqualTo(LocationConstraintsCheck.Success) } @@ -45,7 +45,7 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = false) - val result = checkLocationConstraints(permissionsState, locationActions) + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) assertThat(result).isEqualTo(LocationConstraintsCheck.LocationServiceDisabled) } @@ -58,21 +58,48 @@ class LocationConstraintsCheckTest { ) val locationActions = FakeLocationActions(isLocationEnabled = true) - val result = checkLocationConstraints(permissionsState, locationActions) + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) assertThat(result).isEqualTo(LocationConstraintsCheck.PermissionRationale) } @Test - fun `checkLocationConstraints returns PermissionDenied when permissions denied without rationale`() { + fun `checkLocationConstraints returns PermissionShouldBeRequested when permissions not yet requested`() { + val permissionsState = aPermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + permissionsRequested = false, + ) + val locationActions = FakeLocationActions(isLocationEnabled = true) + + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + + assertThat(result).isEqualTo(LocationConstraintsCheck.PermissionShouldBeRequested) + } + + @Test + fun `checkLocationConstraints returns PermissionDenied when permissions already requested and denied without rationale`() { + val permissionsState = aPermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + permissionsRequested = true, + ) + val locationActions = FakeLocationActions(isLocationEnabled = true) + + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) + + assertThat(result).isEqualTo(LocationConstraintsCheck.PermissionDenied) + } + + @Test + fun `checkLocationConstraints returns NotEnoughPowerLevel when send permissions are not granted`() { val permissionsState = aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, ) val locationActions = FakeLocationActions(isLocationEnabled = true) + val result = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.DEFAULT) - val result = checkLocationConstraints(permissionsState, locationActions) - - assertThat(result).isEqualTo(LocationConstraintsCheck.PermissionDenied) + assertThat(result).isEqualTo(LocationConstraintsCheck.NotEnoughPowerLevel) } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt index 94d909a7ef..9b8ff7a29a 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt @@ -20,6 +20,7 @@ class FakePermissionsPresenter : PermissionsPresenter { private var state = PermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, + permissionsAlreadyRequested = false, eventSink = ::handleEvent, ) set(value) { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManagerTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManagerTest.kt new file mode 100644 index 0000000000..85f0e1c33f --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/DefaultActiveLiveLocationShareManagerTest.kt @@ -0,0 +1,488 @@ +/* + * 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.location.impl.live + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.impl.live.service.LiveLocationSharingCoordinator +import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver +import io.element.android.services.toolbox.api.systemclock.SystemClock +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Instant + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultActiveLiveLocationShareManagerTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `starting the first share starts the coordinator service after the beacon echo and adds an active share`() = runTest { + val startServiceRecorder = lambdaRecorder { } + val stopServiceRecorder = lambdaRecorder { } + val coordinator = createCoordinator( + startService = startServiceRecorder, + stopService = stopServiceRecorder + ) + val beaconInfoUpdates = MutableSharedFlow(replay = 1) + val room = FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ) + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdates, + ).apply { givenGetRoomResult(A_ROOM_ID, room) }, + coordinator = coordinator, + clock = FakeSystemClock(epochMillisResult = 123L), + ) + advanceUntilIdle() + + val result = async { manager.startShare(A_ROOM_ID, 60.minutes) } + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + + assertThat(result.await().isSuccess).isTrue() + assertThat(manager.sharingRoomIds.value).containsExactly(A_ROOM_ID) + assert(startServiceRecorder).isCalledOnce() + assert(stopServiceRecorder).isNeverCalled() + } + + @Test + fun `stopping the last share stops the coordinator service`() = runTest { + val startServiceRecorder = lambdaRecorder { } + val stopServiceRecorder = lambdaRecorder { } + val coordinator = createCoordinator( + startService = startServiceRecorder, + stopService = stopServiceRecorder + ) + val beaconInfoUpdates = MutableSharedFlow(replay = 1) + val room = FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ) + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdates, + ).apply { givenGetRoomResult(A_ROOM_ID, room) }, + coordinator = coordinator, + ) + advanceUntilIdle() + + val startResult = async { manager.startShare(A_ROOM_ID, 15.minutes) } + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + assertThat(startResult.await().isSuccess).isTrue() + + val result = manager.stopShare(A_ROOM_ID) + + assertThat(result.isSuccess).isTrue() + assertThat(manager.sharingRoomIds.value).isEmpty() + assert(startServiceRecorder).isCalledOnce() + assert(stopServiceRecorder).isCalledOnce() + } + + @Test + fun `two managers with the same room id keep isolated state per session`() = runTest { + val coordinator = createCoordinator() + val beaconInfoUpdatesOne = MutableSharedFlow(replay = 1) + val beaconInfoUpdatesTwo = MutableSharedFlow(replay = 1) + val managerOne = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdatesOne, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ), + ) + }, + coordinator = coordinator, + ) + val managerTwo = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID_2, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdatesTwo, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ), + ) + }, + coordinator = coordinator, + ) + advanceUntilIdle() + + val startResult = async { managerOne.startShare(A_ROOM_ID, 15.minutes) } + beaconInfoUpdatesOne.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + assertThat(startResult.await().isSuccess).isTrue() + + assertThat(managerOne.sharingRoomIds.value).containsExactly(A_ROOM_ID) + assertThat(managerTwo.sharingRoomIds.value).isEmpty() + } + + @Test + fun `start share persists room expiry after beacon echo`() = runTest { + val liveLocationStore = createLiveLocationStore() + val coordinator = createCoordinator() + val beaconInfoUpdates = MutableSharedFlow(replay = 1) + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdates, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ), + ) + }, + coordinator = coordinator, + liveLocationStore = liveLocationStore, + clock = FakeSystemClock(epochMillisResult = 123L), + ) + advanceUntilIdle() + + val result = async { manager.startShare(A_ROOM_ID, 15.minutes) } + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + + assertThat(result.await().isSuccess).isTrue() + assertThat(liveLocationStore.getLiveLocationExpiries()).containsKey(A_ROOM_ID) + } + + @Test + fun `stop share removes persisted expiry`() = runTest { + val liveLocationStore = createLiveLocationStore() + val coordinator = createCoordinator() + val beaconInfoUpdates = MutableSharedFlow(replay = 1) + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdates, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ), + ) + }, + coordinator = coordinator, + liveLocationStore = liveLocationStore, + ) + advanceUntilIdle() + + val startResult = async { manager.startShare(A_ROOM_ID, 15.minutes) } + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + assertThat(startResult.await().isSuccess).isTrue() + + manager.stopShare(A_ROOM_ID) + + assertThat(liveLocationStore.getLiveLocationExpiries()).doesNotContainKey(A_ROOM_ID) + } + + @Test + fun `setup restores unexpired stored share and registers coordinator`() = runTest { + val startServiceRecorder = lambdaRecorder { } + val stopServiceRecorder = lambdaRecorder { } + val liveLocationStore = createLiveLocationStore().apply { + setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(10_000L)) + } + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult(A_ROOM_ID, FakeJoinedRoom()) + }, + coordinator = createCoordinator( + startService = startServiceRecorder, + stopService = stopServiceRecorder, + ), + liveLocationStore = liveLocationStore, + clock = FakeSystemClock(epochMillisResult = 1_000L), + ) + + assertThat(manager.sharingRoomIds.value).containsExactly(A_ROOM_ID) + assert(startServiceRecorder).isCalledOnce() + assert(stopServiceRecorder).isNeverCalled() + } + + @Test + fun `setup remotely stops expired stored share and removes it from store`() = runTest { + val stopLiveLocationShareResult = lambdaRecorder> { Result.success(Unit) } + val liveLocationStore = createLiveLocationStore().apply { + setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(1_000L)) + } + createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom(stopLiveLocationShareResult = stopLiveLocationShareResult), + ) + }, + coordinator = createCoordinator(), + liveLocationStore = liveLocationStore, + clock = FakeSystemClock(epochMillisResult = 5_000L), + ) + advanceUntilIdle() + assert(stopLiveLocationShareResult).isCalledOnce() + assertThat(liveLocationStore.getLiveLocationExpiries()).isEmpty() + } + + @Test + fun `stop share closes loaded room and removes persisted expiry when room is not tracked`() = runTest { + val stopLiveLocationShareResult = lambdaRecorder> { Result.success(Unit) } + val room = FakeJoinedRoom(stopLiveLocationShareResult = stopLiveLocationShareResult) + val liveLocationStore = createInMemoryLiveLocationStore() + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult(A_ROOM_ID, room) + }, + coordinator = createCoordinator(), + liveLocationStore = liveLocationStore, + ) + liveLocationStore.setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(10_000L)) + + val result = manager.stopShare(A_ROOM_ID) + + assertThat(result.isSuccess).isTrue() + assert(stopLiveLocationShareResult).isCalledOnce() + assertThat(liveLocationStore.getLiveLocationExpiries()).doesNotContainKey(A_ROOM_ID) + room.baseRoom.assertDestroyed() + } + + @Test + fun `share is automatically stopped when timeout elapses`() = runTest { + val liveLocationStore = createInMemoryLiveLocationStore() + val beaconInfoUpdates = MutableSharedFlow(replay = 1) + val stopLiveLocationShareResult = lambdaRecorder> { Result.success(Unit) } + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdates, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = stopLiveLocationShareResult + ), + ) + }, + coordinator = createCoordinator(), + liveLocationStore = liveLocationStore, + clock = FakeSystemClock(epochMillisResult = 123L), + ) + advanceUntilIdle() + + val startResult = async { manager.startShare(A_ROOM_ID, 1.minutes) } + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + assertThat(startResult.await().isSuccess).isTrue() + + manager.sharingRoomIds.test { + assertThat(awaitItem()).containsExactly(A_ROOM_ID) + assertThat(awaitItem()).isEmpty() + advanceUntilIdle() + assertThat(liveLocationStore.getLiveLocationExpiries()).doesNotContainKey(A_ROOM_ID) + assert(stopLiveLocationShareResult).isCalledExactly(2) + } + } + + @Test + fun `restored share is automatically stopped when remaining timeout elapses`() = runTest { + val liveLocationStore = createInMemoryLiveLocationStore().apply { + setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(6_000L)) + } + val stopLiveLocationShareLambda = lambdaRecorder> { Result.success(Unit) } + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + stopLiveLocationShareResult = stopLiveLocationShareLambda + ), + ) + }, + coordinator = createCoordinator(), + liveLocationStore = liveLocationStore, + clock = FakeSystemClock(epochMillisResult = 1_000L), + ) + + manager.sharingRoomIds.test { + assertThat(awaitItem()).containsExactly(A_ROOM_ID) + assertThat(awaitItem()).isEmpty() + advanceUntilIdle() + assertThat(liveLocationStore.getLiveLocationExpiries()).doesNotContainKey(A_ROOM_ID) + assert(stopLiveLocationShareLambda).isCalledOnce() + } + } + + @Test + fun `session deleted clears local state`() = runTest { + val startServiceRecorder = lambdaRecorder { } + val stopServiceRecorder = lambdaRecorder { } + val liveLocationStore = createInMemoryLiveLocationStore() + val sessionObserver = FakeSessionObserver() + val beaconInfoUpdates = MutableSharedFlow(replay = 1) + val manager = createManager( + client = FakeMatrixClient( + sessionId = A_SESSION_ID, + sessionCoroutineScope = backgroundScope, + ownBeaconInfoUpdates = beaconInfoUpdates, + ).apply { + givenGetRoomResult( + A_ROOM_ID, + FakeJoinedRoom( + startLiveLocationShareResult = { Result.success(AN_EVENT_ID) }, + stopLiveLocationShareResult = { Result.success(Unit) }, + ), + ) + }, + coordinator = createCoordinator( + startService = startServiceRecorder, + stopService = stopServiceRecorder, + ), + liveLocationStore = liveLocationStore, + sessionObserver = sessionObserver, + ) + advanceUntilIdle() + + val firstStart = async { manager.startShare(A_ROOM_ID, 15.minutes) } + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + assertThat(firstStart.await().isSuccess).isTrue() + + sessionObserver.onSessionDeleted(A_SESSION_ID.value) + advanceUntilIdle() + + assertThat(manager.sharingRoomIds.value).isEmpty() + assertThat(liveLocationStore.getLiveLocationExpiries()).doesNotContainKey(A_ROOM_ID) + assert(startServiceRecorder).isCalledOnce() + assert(stopServiceRecorder).isCalledOnce() + + val secondStart = async { manager.startShare(A_ROOM_ID, 15.minutes) } + advanceUntilIdle() + assertThat(secondStart.isCompleted).isFalse() + + beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true)) + assertThat(secondStart.await().isSuccess).isTrue() + } + + private suspend fun createManager( + client: FakeMatrixClient = FakeMatrixClient(sessionId = A_SESSION_ID), + coordinator: LiveLocationSharingCoordinator = createCoordinator(), + liveLocationStore: LiveLocationStore = createLiveLocationStore(), + clock: SystemClock = FakeSystemClock(), + sessionObserver: SessionObserver = FakeSessionObserver(), + ): DefaultActiveLiveLocationShareManager { + return DefaultActiveLiveLocationShareManager( + matrixClient = client, + coordinator = coordinator, + liveLocationStore = liveLocationStore, + clock = clock, + sessionObserver = sessionObserver, + ).apply { + setup() + } + } + + private fun createCoordinator( + startService: () -> Unit = {}, + stopService: () -> Unit = {}, + nowMillis: () -> Long = { 0L }, + ): LiveLocationSharingCoordinator { + return LiveLocationSharingCoordinator( + startService = startService, + stopService = stopService, + nowMillis = nowMillis, + ) + } + + private fun createLiveLocationStore( + sessionId: io.element.android.libraries.matrix.api.core.SessionId = A_SESSION_ID, + preferenceDataStoreFactory: PreferenceDataStoreFactory = FakePreferenceDataStoreFactory(), + ): LiveLocationStore { + return LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = sessionId, + ) + } + + private fun createInMemoryLiveLocationStore( + sessionId: io.element.android.libraries.matrix.api.core.SessionId = A_SESSION_ID, + ): LiveLocationStore { + val preferenceDataStoreFactory = object : PreferenceDataStoreFactory { + override fun create(name: String): DataStore { + var preferences: Preferences = emptyPreferences() + return object : DataStore { + override val data: Flow + get() = flowOf(preferences) + + override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences { + preferences = transform(preferences) + return preferences + } + } + } + } + return createLiveLocationStore( + sessionId = sessionId, + preferenceDataStoreFactory = preferenceDataStoreFactory, + ) + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/LiveLocationSharingCoordinatorTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/LiveLocationSharingCoordinatorTest.kt new file mode 100644 index 0000000000..6cee53a482 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/live/LiveLocationSharingCoordinatorTest.kt @@ -0,0 +1,122 @@ +/* + * 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.location.impl.live + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.Location +import io.element.android.features.location.impl.live.service.LiveLocationReceiver +import io.element.android.features.location.impl.live.service.LiveLocationSharingCoordinator +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LiveLocationSharingCoordinatorTest { + @Test + fun `first registration starts the service and last unregister stops it`() = runTest { + var startCount = 0 + var stopCount = 0 + val coordinator = LiveLocationSharingCoordinator( + startService = { startCount++ }, + stopService = { stopCount++ }, + nowMillis = { 0L }, + ) + + coordinator.register(A_SESSION_ID, liveLocationReceiver()) + coordinator.unregister(A_SESSION_ID) + + assertThat(startCount).isEqualTo(1) + assertThat(stopCount).isEqualTo(1) + } + + @Test + fun `dispatch isolates receiver failures and still reaches later receivers`() = runTest { + val delivered = mutableListOf() + val coordinator = LiveLocationSharingCoordinator( + startService = { }, + stopService = { }, + nowMillis = { 4_000L }, + ) + + coordinator.register(A_SESSION_ID, liveLocationReceiver { error("boom") }) + coordinator.register(A_SESSION_ID_2, liveLocationReceiver { delivered += it }) + coordinator.dispatch(Location(lat = 1.0, lon = 2.0, accuracy = 3f)) + + assertThat(delivered).containsExactly(Location(lat = 1.0, lon = 2.0, accuracy = 3f)) + } + + @Test + fun `dispatch delivers first location immediately`() = runTest { + var nowMillis = 4_000L + val delivered = mutableListOf() + val coordinator = LiveLocationSharingCoordinator( + startService = { }, + stopService = { }, + nowMillis = { nowMillis }, + ) + + coordinator.register(A_SESSION_ID, liveLocationReceiver { delivered += it }) + + val firstLocation = Location(lat = 1.0, lon = 2.0, accuracy = 3f) + + coordinator.dispatch(firstLocation) + + assertThat(delivered).containsExactly(firstLocation) + } + + @Test + fun `dispatch drops updates inside the throttle window`() = runTest { + var nowMillis = 4_000L + val delivered = mutableListOf() + val coordinator = LiveLocationSharingCoordinator( + startService = { }, + stopService = { }, + nowMillis = { nowMillis }, + ) + + coordinator.register(A_SESSION_ID, liveLocationReceiver { delivered += it }) + + val firstLocation = Location(lat = 1.0, lon = 2.0, accuracy = 3f) + val secondLocation = Location(lat = 4.0, lon = 5.0, accuracy = 6f) + + coordinator.dispatch(firstLocation) + nowMillis += 500 + coordinator.dispatch(secondLocation) + + assertThat(delivered).containsExactly(firstLocation) + } + + @Test + fun `dispatch delivers next update after the throttle window elapses`() = runTest { + var nowMillis = 4_000L + val delivered = mutableListOf() + val coordinator = LiveLocationSharingCoordinator( + startService = { }, + stopService = { }, + nowMillis = { nowMillis }, + ) + + coordinator.register(A_SESSION_ID, liveLocationReceiver { delivered += it }) + + val firstLocation = Location(lat = 1.0, lon = 2.0, accuracy = 3f) + val secondLocation = Location(lat = 4.0, lon = 5.0, accuracy = 6f) + + coordinator.dispatch(firstLocation) + nowMillis += 3_000 + coordinator.dispatch(secondLocation) + + assertThat(delivered).containsExactly(firstLocation, secondLocation).inOrder() + } +} + +private fun liveLocationReceiver( + onLocation: suspend (Location) -> Unit = {}, +) = object : LiveLocationReceiver { + override suspend fun onLocationUpdate(location: Location) = onLocation(location) + override suspend fun onUnrecoverableError() = Unit +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt index edd000e02c..49df684666 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt @@ -11,17 +11,21 @@ package io.element.android.features.location.impl.share import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter +import io.element.android.features.location.impl.live.LiveLocationStore +import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.dateformatter.test.FakeDurationFormatter -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -30,24 +34,30 @@ class DefaultShareLocationEntryPointTest { val instantTaskExecutorRule = InstantTaskExecutorRule() @Test - fun `test node builder`() { + fun `test node builder`() = runTest { val entryPoint = DefaultShareLocationEntryPoint() val parentNode = TestParentNode.create { buildContext, plugins -> + val room = FakeJoinedRoom() ShareLocationNode( buildContext = buildContext, plugins = plugins, presenterFactory = { timelineMode: Timeline.Mode -> ShareLocationPresenter( permissionsPresenterFactory = { FakePermissionsPresenter() }, - room = FakeJoinedRoom(), + room = room, timelineMode = timelineMode, analyticsService = FakeAnalyticsService(), messageComposerContext = FakeMessageComposerContext(), locationActions = FakeLocationActions(), buildMeta = aBuildMeta(), - featureFlagService = FakeFeatureFlagService(), client = FakeMatrixClient(), durationFormatter = FakeDurationFormatter(), + liveLocationShareManager = FakeActiveLiveLocationShareManager(), + liveLocationStore = LiveLocationStore( + preferenceDataStoreFactory = FakePreferenceDataStoreFactory(), + sessionId = room.sessionId, + ), + userLocationStateFactory = FakeUserLocationStateFactory(), ) }, analyticsService = FakeAnalyticsService(), 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 46bc4a55df..f4cc135e9e 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 @@ -10,40 +10,63 @@ package io.element.android.features.location.impl.share +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer import io.element.android.features.location.api.Location import io.element.android.features.location.impl.aPermissionsState +import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState +import io.element.android.features.location.impl.live.LiveLocationStore +import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.dateformatter.test.FakeDurationFormatter -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta +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.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert 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.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours class ShareLocationPresenterTest { @get:Rule @@ -54,25 +77,33 @@ class ShareLocationPresenterTest { private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeLocationActions = FakeLocationActions() private val fakeBuildMeta = aBuildMeta(applicationName = "app name") - private val fakeFeatureFlagService = FakeFeatureFlagService() - private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID) + private val fakeMatrixClient = FakeMatrixClient( + sessionId = A_USER_ID, + getMapStyleUrlResult = { Result.success(null) }, + ) private val durationFormatter = FakeDurationFormatter() - private fun createShareLocationPresenter( + private fun TestScope.createShareLocationPresenter( joinedRoom: JoinedRoom = FakeJoinedRoom(), + timelineMode: Timeline.Mode = Timeline.Mode.Live, locationActions: FakeLocationActions = fakeLocationActions, + liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), + liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId), + client: FakeMatrixClient = fakeMatrixClient, ): ShareLocationPresenter = ShareLocationPresenter( permissionsPresenterFactory = { fakePermissionsPresenter }, room = joinedRoom, - timelineMode = Timeline.Mode.Live, + timelineMode = timelineMode, analyticsService = fakeAnalyticsService, messageComposerContext = fakeMessageComposerContext, locationActions = locationActions, buildMeta = fakeBuildMeta, - featureFlagService = fakeFeatureFlagService, - client = fakeMatrixClient, + client = client, durationFormatter = durationFormatter, + liveLocationShareManager = liveLocationShareManager, + liveLocationStore = liveLocationStore, + userLocationStateFactory = FakeUserLocationStateFactory(), ) @Test @@ -85,12 +116,28 @@ class ShareLocationPresenterTest { ) val shareLocationPresenter = createShareLocationPresenter() + shareLocationPresenter.test { + val state = awaitFirstItem() + assertThat(state.customMapStyleUrl.isLoading()).isFalse() + assertThat(state.trackUserLocation).isTrue() + assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None)) + } + } + + @Test + fun `present - non-null customMapStyleUrl`() = runTest { + val shareLocationPresenter = createShareLocationPresenter( + client = FakeMatrixClient( + sessionId = A_USER_ID, + getMapStyleUrlResult = { Result.success("aUrl") }, + ) + ) shareLocationPresenter.test { skipItems(1) val state = awaitItem() - assertThat(state.trackUserLocation).isTrue() - assertThat(state.hasLocationPermission).isTrue() - assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None)) + assertThat(state.customMapStyleUrl.isLoading()).isTrue() + val finalState = awaitItem() + assertThat(finalState.customMapStyleUrl.dataOrNull()).isEqualTo("aUrl") } } @@ -107,14 +154,30 @@ class ShareLocationPresenterTest { moleculeFlow(RecompositionMode.Immediate) { shareLocationPresenter.present() }.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isTrue() - assertThat(initialState.hasLocationPermission).isTrue() assertThat(initialState.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None)) } } + @Test + fun `initial state with permissions not yet requested triggers permission request`() = runTest { + val shareLocationPresenter = createShareLocationPresenter() + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + permissionsRequested = false, + ) + ) + + shareLocationPresenter.test { + skipItems(2) + cancelAndIgnoreRemainingEvents() + assertThat(fakePermissionsPresenter.events).contains(PermissionsEvents.RequestPermissions) + } + } + @Test fun `initial state with permissions denied`() = runTest { val shareLocationPresenter = createShareLocationPresenter() @@ -122,16 +185,15 @@ class ShareLocationPresenterTest { aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, + permissionsRequested = true, ) ) moleculeFlow(RecompositionMode.Immediate) { shareLocationPresenter.present() }.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isFalse() - assertThat(initialState.hasLocationPermission).isFalse() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied) ) @@ -149,10 +211,8 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isFalse() - assertThat(initialState.hasLocationPermission).isFalse() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale) ) @@ -171,10 +231,8 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isFalse() - assertThat(initialState.hasLocationPermission).isTrue() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled) ) @@ -192,8 +250,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.trackUserLocation).isTrue() initialState.eventSink(ShareLocationEvent.StopTrackingUserLocation) @@ -213,8 +270,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale) ) @@ -236,7 +292,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(ShareLocationEvent.RequestPermissions) // Wait for dialog to be dismissed @@ -254,12 +310,12 @@ class ShareLocationPresenterTest { aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, + permissionsRequested = true, ) ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink(ShareLocationEvent.OpenAppSettings) val settingsOpenedState = awaitItem() @@ -280,8 +336,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() assertThat(initialState.dialogState).isEqualTo( ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled) ) @@ -296,7 +351,15 @@ class ShareLocationPresenterTest { @Test fun `ShowLiveLocationDurationPicker shows duration dialog when constraints pass`() = runTest { - val shareLocationPresenter = createShareLocationPresenter() + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = grantedSendLiveLocationPermissions() + ) + ) + val locationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId).apply { + setAcceptedLiveLocationDisclaimer().getOrThrow() + } + val shareLocationPresenter = createShareLocationPresenter(joinedRoom = joinedRoom, liveLocationStore = locationStore) fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.AllGranted, @@ -305,9 +368,8 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() - initialState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) + val initialState = awaitFirstItem() + initialState.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val durationDialogState = awaitItem() assertThat(durationDialogState.dialogState).isInstanceOf(ShareLocationState.Dialog.LiveLocationDurations::class.java) @@ -316,23 +378,174 @@ class ShareLocationPresenterTest { } @Test - fun `ShowLiveLocationDurationPicker shows constraint dialog when permissions denied`() = runTest { - val shareLocationPresenter = createShareLocationPresenter() + fun `ShowLiveLocationDurationPicker shows disclaimer when acceptance is missing`() = runTest { + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = grantedSendLiveLocationPermissions() + ) + ) + val presenter = createShareLocationPresenter(joinedRoom = room) fakePermissionsPresenter.givenState( aPermissionsState( - permissions = PermissionsState.Permissions.NoneGranted, + permissions = PermissionsState.Permissions.AllGranted, shouldShowRationale = false, ) ) + presenter.test { + val state = awaitFirstItem() + + state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + val dialogState = awaitItem() + + assertThat(dialogState.dialogState).isEqualTo(ShareLocationState.Dialog.LiveLocationDisclaimer) + } + } + + @Test + fun `AcceptLiveLocationDisclaimer persists acceptance and shows durations`() = runTest { + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = grantedSendLiveLocationPermissions() + ) + ) + val locationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId) + val presenter = createShareLocationPresenter(joinedRoom = joinedRoom, liveLocationStore = locationStore) + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + presenter.test { + val state = awaitFirstItem() + state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + awaitItem() + + state.eventSink(ShareLocationEvent.AcceptLiveLocationDisclaimer) + val durationState = awaitItem() + + assertThat(locationStore.hasAcceptedLiveLocationDisclaimer()).isTrue() + assertThat(durationState.dialogState).isInstanceOf(ShareLocationState.Dialog.LiveLocationDurations::class.java) + } + } + + @Test + fun `AcceptLiveLocationDisclaimer keeps disclaimer gate active when persistence fails`() = runTest { + val joinedRoom = FakeJoinedRoom() + val presenter = createShareLocationPresenter( + joinedRoom = joinedRoom, + liveLocationStore = createFailingLiveLocationStore(sessionId = joinedRoom.sessionId), + ) + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + presenter.test { + val state = awaitFirstItem() + state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + val disclaimerState = awaitItem() + + disclaimerState.eventSink(ShareLocationEvent.AcceptLiveLocationDisclaimer) + advanceUntilIdle() + + expectNoEvents() + } + } + + @Test + fun `ShowLiveLocationDurationPicker bypasses disclaimer when already accepted`() = runTest { + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = grantedSendLiveLocationPermissions() + ) + ) + val locationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId).apply { + setAcceptedLiveLocationDisclaimer().getOrThrow() + } + val presenter = createShareLocationPresenter(joinedRoom = joinedRoom, liveLocationStore = locationStore) + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + presenter.test { + val state = awaitFirstItem() + + state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + val durationState = awaitItem() + + assertThat(durationState.dialogState).isInstanceOf(ShareLocationState.Dialog.LiveLocationDurations::class.java) + } + } + + @Test + fun `ShowLiveLocationDurationPicker uses the active session disclaimer state`() = runTest { + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + sessionId = SessionId("@alice:server"), + roomPermissions = grantedSendLiveLocationPermissions() + ), + ) + createLiveLocationStore(sessionId = SessionId("@bob:server")) + .setAcceptedLiveLocationDisclaimer() + .getOrThrow() + val presenter = createShareLocationPresenter( + joinedRoom = joinedRoom, + liveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId), + ) + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + presenter.test { + val state = awaitFirstItem() + + state.eventSink(ShareLocationEvent.InitiateLiveLocationShare) + val dialogState = awaitItem() + + assertThat(dialogState.dialogState).isEqualTo(ShareLocationState.Dialog.LiveLocationDisclaimer) + } + } + + @Test + fun `ShowLiveLocationDurationPicker shows constraint dialog when permissions denied`() = runTest { + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = grantedSendLiveLocationPermissions() + ) + ) + val locationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId).apply { + setAcceptedLiveLocationDisclaimer().getOrThrow() + } + val shareLocationPresenter = createShareLocationPresenter( + joinedRoom = joinedRoom, + liveLocationStore = locationStore, + ) + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + permissionsRequested = true, + ) + ) + shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() - // Dismiss initial dialog + val initialState = awaitFirstItem() + // Dismiss initial dialog to allow re-triggering it initialState.eventSink(ShareLocationEvent.DismissDialog) val dismissedState = awaitItem() - dismissedState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) + dismissedState.eventSink(ShareLocationEvent.InitiateLiveLocationShare) val constraintDialogState = awaitItem() assertThat(constraintDialogState.dialogState).isEqualTo( @@ -361,8 +574,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - skipItems(1) - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink( ShareLocationEvent.ShareStaticLocation( @@ -415,7 +627,7 @@ class ShareLocationPresenterTest { ) shareLocationPresenter.test { - val initialState = awaitItem() + val initialState = awaitFirstItem() initialState.eventSink( ShareLocationEvent.ShareStaticLocation( @@ -447,4 +659,88 @@ class ShareLocationPresenterTest { cancelAndIgnoreRemainingEvents() } } + + @Test + fun `StartLiveLocationShare event calls manager startShare`() = runTest { + val startShareLambda = lambdaRecorder { _: RoomId, _: Duration -> Result.success(Unit) } + val manager = FakeActiveLiveLocationShareManager( + startShareLambda = startShareLambda, + ) + val shareLocationPresenter = createShareLocationPresenter(liveLocationShareManager = manager) + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + shareLocationPresenter.test { + val state = awaitFirstItem() + state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration = 1.hours)) + advanceUntilIdle() + assert(startShareLambda).isCalledOnce().with( + value(A_ROOM_ID), + value(1.hours) + ) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `canShareLiveLocation is true in live timeline`() = runTest { + val shareLocationPresenter = createShareLocationPresenter( + timelineMode = Timeline.Mode.Live, + ) + shareLocationPresenter.test { + val state = awaitFirstItem() + assertThat(state.canShareLiveLocation).isTrue() + } + } + + @Test + fun `canShareLiveLocation is false in thread timeline`() = runTest { + val shareLocationPresenter = createShareLocationPresenter( + timelineMode = Timeline.Mode.Thread(A_THREAD_ID), + ) + shareLocationPresenter.test { + val state = awaitFirstItem() + assertThat(state.canShareLiveLocation).isFalse() + } + } + + private suspend fun ReceiveTurbine.awaitFirstItem(): T { + skipItems(2) + return awaitItem() + } } + +private fun createLiveLocationStore( + sessionId: SessionId = A_SESSION_ID, + preferenceDataStoreFactory: PreferenceDataStoreFactory = FakePreferenceDataStoreFactory(), +): LiveLocationStore { + return LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = sessionId, + ) +} + +private fun createFailingLiveLocationStore(sessionId: SessionId = A_SESSION_ID): LiveLocationStore { + val failingPreferenceDataStoreFactory = object : PreferenceDataStoreFactory { + override fun create(name: String): DataStore = object : DataStore { + override val data: Flow = flowOf(emptyPreferences()) + + override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences { + error("Failed to update preferences") + } + } + } + return createLiveLocationStore( + sessionId = sessionId, + preferenceDataStoreFactory = failingPreferenceDataStoreFactory, + ) +} + +private fun grantedSendLiveLocationPermissions(): FakeRoomPermissions = FakeRoomPermissions( + canSendState = { it is StateEventType.BeaconInfo }, + canSendMessage = { it is MessageEventType.Beacon } +) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt index 317fbf8fed..c53e463280 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationViewTest.kt @@ -5,16 +5,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.location.impl.share import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings @@ -23,130 +25,155 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ShareLocationViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ShareLocationViewTest : RobolectricTest() { @Test - fun `test back action`() { + fun `test back action`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setShareLocationView( + setShareLocationView( state = aShareLocationState( eventSink = eventsRecorder ), navigateUp = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `test fab click`() { + fun `test fab click`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() + onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShareLocationEvent.StartTrackingUserLocation) } @Test - fun `when permission denied is displayed user can open the settings`() { + fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.OpenAppSettings) } @Test - fun `when permission denied is displayed user can close the dialog`() { + fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } @Test - fun `when permission rationale is displayed user can request permissions`() { + fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.RequestPermissions) } @Test - fun `when permission rationale is displayed user can close the dialog`() { + fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale), eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } @Test - fun `when location service disabled is displayed user can open location settings`() { + fun `when location service disabled is displayed user can open location settings`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), - hasLocationPermission = true, eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShareLocationEvent.OpenLocationSettings) } @Test - fun `when location service disabled is displayed user can close the dialog`() { + fun `when location service disabled is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShareLocationView( + setShareLocationView( aShareLocationState( dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled), - hasLocationPermission = true, eventSink = eventsRecorder ), navigateUp = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) + } + + @Test + fun `when disclaimer is displayed user can accept`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setShareLocationView( + aShareLocationState( + dialogState = ShareLocationState.Dialog.LiveLocationDisclaimer, + eventSink = eventsRecorder, + canShareLiveLocation = true, + ), + navigateUp = EnsureNeverCalled(), + ) + + clickOn(CommonStrings.action_accept) + eventsRecorder.assertSingle(ShareLocationEvent.AcceptLiveLocationDisclaimer) + } + + @Test + fun `when disclaimer is displayed user can decline`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setShareLocationView( + aShareLocationState( + dialogState = ShareLocationState.Dialog.LiveLocationDisclaimer, + eventSink = eventsRecorder, + canShareLiveLocation = true, + ), + navigateUp = EnsureNeverCalled(), + ) + + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(ShareLocationEvent.DismissDialog) } } -private fun AndroidComposeTestRule.setShareLocationView( +private fun AndroidComposeUiTest.setShareLocationView( state: ShareLocationState, navigateUp: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt index 91df447e2a..23b7e9bf4d 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt @@ -14,10 +14,13 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.location.api.ShowLocationMode +import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter +import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.services.analytics.test.FakeAnalyticsService @@ -34,6 +37,7 @@ class DefaultShowLocationEntryPointTest { fun `test node builder`() { val entryPoint = DefaultShowLocationEntryPoint() val parentNode = TestParentNode.create { buildContext, plugins -> + val joinedRoom = FakeJoinedRoom() ShowLocationNode( buildContext = buildContext, plugins = plugins, @@ -45,7 +49,10 @@ class DefaultShowLocationEntryPointTest { buildMeta = aBuildMeta(), dateFormatter = FakeDateFormatter(), stringProvider = FakeStringProvider(), - joinedRoom = FakeJoinedRoom(), + joinedRoom = joinedRoom, + client = FakeMatrixClient(), + liveLocationShareManager = FakeActiveLiveLocationShareManager(), + userLocationStateFactory = FakeUserLocationStateFactory(), ) }, analyticsService = FakeAnalyticsService(), diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparatorTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparatorTest.kt index 4042cb4c0c..0b8e04abf8 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparatorTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/LiveLocationShareComparatorTest.kt @@ -9,7 +9,7 @@ package io.element.android.features.location.impl.show import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.location.LiveLocationShare +import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare import org.junit.Test class LiveLocationShareComparatorTest { @@ -55,15 +55,3 @@ class LiveLocationShareComparatorTest { assertThat(sortedShares).containsExactly(newerShare, olderShare).inOrder() } } - -private fun aLiveLocationShare( - userId: UserId, - startTimestamp: Long, -): LiveLocationShare { - return LiveLocationShare( - userId = userId, - lastLocation = null, - startTimestamp = startTimestamp, - endTimestamp = startTimestamp + 1_000L, - ) -} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index f38e8dae60..b579a79e4f 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -15,28 +15,33 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.impl.aPermissionsState +import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState +import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.libraries.dateformatter.test.FakeDateFormatter +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.location.AssetType -import io.element.android.libraries.matrix.api.room.location.LastLocation import io.element.android.libraries.matrix.api.room.location.LiveLocationShare +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration.Companion.hours @OptIn(ExperimentalCoroutinesApi::class) class ShowLocationPresenterTest { @@ -60,6 +65,8 @@ class ShowLocationPresenterTest { ), locationActions: FakeLocationActions = fakeLocationActions, joinedRoom: JoinedRoom = FakeJoinedRoom(), + client: MatrixClient = FakeMatrixClient(), + liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ) = ShowLocationPresenter( mode = mode, permissionsPresenterFactory = { fakePermissionsPresenter }, @@ -68,51 +75,24 @@ class ShowLocationPresenterTest { dateFormatter = fakeDateFormatter, stringProvider = FakeStringProvider(), joinedRoom = joinedRoom, + client = client, + liveLocationShareManager = liveLocationShareManager, + userLocationStateFactory = FakeUserLocationStateFactory(), ) @Test - fun `emits initial state with no location permission`() = runTest { - fakePermissionsPresenter.givenState( - aPermissionsState( - permissions = PermissionsState.Permissions.NoneGranted, - shouldShowRationale = false, + fun `present - non-null customMapStyleUrl`() = runTest { + val shareLocationPresenter = createShowLocationPresenter( + client = FakeMatrixClient( + sessionId = A_USER_ID, + getMapStyleUrlResult = { Result.success("aUrl") }, ) ) - - val presenter = createShowLocationPresenter() - presenter.test { - val initialState = awaitItem() - assertThat(initialState.hasLocationPermission).isFalse() - assertThat(initialState.isTrackMyLocation).isFalse() - } - } - - @Test - fun `emits initial state location permission denied once`() = runTest { - fakePermissionsPresenter.givenState( - aPermissionsState( - permissions = PermissionsState.Permissions.NoneGranted, - shouldShowRationale = true, - ) - ) - - val presenter = createShowLocationPresenter() - presenter.test { - val initialState = awaitItem() - assertThat(initialState.hasLocationPermission).isFalse() - assertThat(initialState.isTrackMyLocation).isFalse() - } - } - - @Test - fun `emits initial state with location permission`() = runTest { - fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) - - val presenter = createShowLocationPresenter() - presenter.test { - val initialState = awaitItem() - assertThat(initialState.hasLocationPermission).isTrue() - assertThat(initialState.isTrackMyLocation).isFalse() + shareLocationPresenter.test { + val state = awaitItem() + assertThat(state.customMapStyleUrl.isLoading()).isTrue() + val finalState = awaitItem() + assertThat(finalState.customMapStyleUrl.dataOrNull()).isEqualTo("aUrl") } } @@ -122,9 +102,8 @@ class ShowLocationPresenterTest { val presenter = createShowLocationPresenter() presenter.test { - val initialState = awaitItem() - assertThat(initialState.hasLocationPermission).isTrue() - assertThat(initialState.isTrackMyLocation).isFalse() + assertThat(awaitItem().isTrackMyLocation).isFalse() + assertThat(awaitItem().isTrackMyLocation).isTrue() } } @@ -143,18 +122,18 @@ class ShowLocationPresenterTest { fun `centers on user location`() = runTest { fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) - val presenter = createShowLocationPresenter() + val presenter = createShowLocationPresenter( + client = FakeMatrixClient( + getMapStyleUrlResult = { Result.success(null) } + ) + ) presenter.test { val initialState = awaitItem() - assertThat(initialState.hasLocationPermission).isTrue() assertThat(initialState.isTrackMyLocation).isFalse() initialState.eventSink(ShowLocationEvent.TrackMyLocation(true)) val trackMyLocationState = awaitItem() - delay(1) - - assertThat(trackMyLocationState.hasLocationPermission).isTrue() assertThat(trackMyLocationState.isTrackMyLocation).isTrue() // Swipe the map to switch mode @@ -162,7 +141,6 @@ class ShowLocationPresenterTest { val trackLocationDisabledState = awaitItem() assertThat(trackLocationDisabledState.dialogState).isEqualTo(LocationConstraintsDialogState.None) assertThat(trackLocationDisabledState.isTrackMyLocation).isFalse() - assertThat(trackLocationDisabledState.hasLocationPermission).isTrue() } } @@ -185,14 +163,12 @@ class ShowLocationPresenterTest { val trackLocationState = awaitItem() assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale) assertThat(trackLocationState.isTrackMyLocation).isFalse() - assertThat(trackLocationState.hasLocationPermission).isFalse() // Dismiss the dialog initialState.eventSink(ShowLocationEvent.DismissDialog) val dialogDismissedState = awaitItem() assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None) assertThat(dialogDismissedState.isTrackMyLocation).isFalse() - assertThat(dialogDismissedState.hasLocationPermission).isFalse() } } @@ -205,7 +181,7 @@ class ShowLocationPresenterTest { ) ) val presenter = createShowLocationPresenter() - presenter.test { + presenter.test { // Skip initial state val initialState = awaitItem() @@ -214,7 +190,6 @@ class ShowLocationPresenterTest { val trackLocationState = awaitItem() assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale) assertThat(trackLocationState.isTrackMyLocation).isFalse() - assertThat(trackLocationState.hasLocationPermission).isFalse() // Continue the dialog sends permission request to the permissions presenter trackLocationState.eventSink(ShowLocationEvent.RequestPermissions) @@ -222,12 +197,34 @@ class ShowLocationPresenterTest { } } + @Test + fun `TrackMyLocation with permissions not yet requested triggers permission request`() = runTest { + fakePermissionsPresenter.givenState( + aPermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + permissionsRequested = false, + ) + ) + + val presenter = createShowLocationPresenter() + presenter.test { + val initialState = awaitItem() + + initialState.eventSink(ShowLocationEvent.TrackMyLocation(true)) + + assertThat(fakePermissionsPresenter.events).contains(PermissionsEvents.RequestPermissions) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `permission denied dialog dismiss`() = runTest { fakePermissionsPresenter.givenState( aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, + permissionsRequested = true, ) ) @@ -241,14 +238,12 @@ class ShowLocationPresenterTest { val trackLocationState = awaitItem() assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionDenied) assertThat(trackLocationState.isTrackMyLocation).isFalse() - assertThat(trackLocationState.hasLocationPermission).isFalse() // Dismiss the dialog initialState.eventSink(ShowLocationEvent.DismissDialog) val dialogDismissedState = awaitItem() assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None) assertThat(dialogDismissedState.isTrackMyLocation).isFalse() - assertThat(dialogDismissedState.hasLocationPermission).isFalse() } } @@ -258,6 +253,7 @@ class ShowLocationPresenterTest { aPermissionsState( permissions = PermissionsState.Permissions.NoneGranted, shouldShowRationale = false, + permissionsRequested = true, ) ) @@ -296,7 +292,6 @@ class ShowLocationPresenterTest { val presenter = createShowLocationPresenter() presenter.test { val initialState = awaitItem() - assertThat(initialState.hasLocationPermission).isTrue() // Try to track location when location services are disabled initialState.eventSink(ShowLocationEvent.TrackMyLocation(true)) @@ -463,24 +458,51 @@ class ShowLocationPresenterTest { assertThat(state.isSheetDraggable).isFalse() } } -} -private fun aLiveLocationShare( - userId: UserId, - geoUri: String = "geo:48.8584,2.2945", - timestamp: Long = 0L, - startTimestamp: Long = 0L, - endTimestamp: Long = Long.MAX_VALUE, - assetType: AssetType = AssetType.SENDER, -): LiveLocationShare { - return LiveLocationShare( - userId = userId, - lastLocation = LastLocation( - geoUri = geoUri, - timestamp = timestamp, - assetType = assetType, - ), - startTimestamp = startTimestamp, - endTimestamp = endTimestamp, - ) + @Test + fun `static mode never hides user location puck`() = runTest { + val presenter = createShowLocationPresenter() + presenter.test { + val state = awaitItem() + assertThat(state.hideUserLocationPuck).isFalse() + } + } + + @Test + fun `live mode does not hide user location puck when not sharing`() = runTest { + val presenter = createShowLocationPresenter( + mode = ShowLocationMode.Live(senderId = A_USER_ID), + ) + presenter.test { + val state = awaitItem() + assertThat(state.hideUserLocationPuck).isFalse() + } + } + + @Test + fun `live mode user location state uses own share position when sharing`() = runTest { + val ownLiveLocationShare = aLiveLocationShare() + val fakeRoom = FakeJoinedRoom( + liveLocationSharesFlow = MutableStateFlow(listOf(ownLiveLocationShare)) + ) + val manager = FakeActiveLiveLocationShareManager( + startShareLambda = { _, _ -> Result.success(Unit) } + ) + manager.startShare(fakeRoom.roomId, 1.hours) + + val presenter = createShowLocationPresenter( + mode = ShowLocationMode.Live(senderId = A_USER_ID), + joinedRoom = fakeRoom, + liveLocationShareManager = manager, + ) + val ownLocation = ownLiveLocationShare.lastLocation?.geoUri?.let(Location::fromGeoUri) + presenter.test { + skipItems(1) + val state = awaitItem() + assertThat(state.hideUserLocationPuck).isTrue() + val location = requireNotNull(state.userLocationState.location) + assertThat(location.position.value.latitude).isEqualTo(ownLocation?.lat) + assertThat(location.position.value.longitude).isEqualTo(ownLocation?.lon) + } + } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt index fecbbdbf89..053c26c179 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt @@ -6,17 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.location.impl.show import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.libraries.testtags.TestTags @@ -26,115 +28,110 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ShowLocationViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ShowLocationViewTest : RobolectricTest() { @Test - fun `test back action`() { + fun `test back action`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setShowLocationView( + setShowLocationView( state = aShowLocationState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `test share action`() { + fun `test share action`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - val shareContentDescription = rule.activity.getString(CommonStrings.action_share) - rule.onNodeWithContentDescription(shareContentDescription).performClick() + val shareContentDescription = activity!!.getString(CommonStrings.action_share) + onNodeWithContentDescription(shareContentDescription).performClick() // The default aStaticLocationMode uses Location(1.23, 2.34, 4f) eventsRecorder.assertSingle(ShowLocationEvent.Share(Location(1.23, 2.34, 4f))) } @Test - fun `test fab click`() { + fun `test fab click`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() + onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShowLocationEvent.TrackMyLocation(true)) } @Test - fun `when permission denied is displayed user can open the settings`() { + fun `when permission denied is displayed user can open the settings`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShowLocationEvent.OpenAppSettings) } @Test - fun `when permission denied is displayed user can close the dialog`() { + fun `when permission denied is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) } @Test - fun `when permission rationale is displayed user can request permissions`() { + fun `when permission rationale is displayed user can request permissions`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ShowLocationEvent.RequestPermissions) } @Test - fun `when permission rationale is displayed user can close the dialog`() { + fun `when permission rationale is displayed user can close the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setShowLocationView( + setShowLocationView( aShowLocationState( constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ShowLocationEvent.DismissDialog) } } -private fun AndroidComposeTestRule.setShowLocationView( +private fun AndroidComposeUiTest.setShowLocationView( state: ShowLocationState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/store/LiveLocationStoreTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/store/LiveLocationStoreTest.kt new file mode 100644 index 0000000000..c42469e705 --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/store/LiveLocationStoreTest.kt @@ -0,0 +1,129 @@ +/* + * 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.location.impl.store + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.mutablePreferencesOf +import androidx.datastore.preferences.core.stringPreferencesKey +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.impl.live.LiveLocationStore +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Instant + +class LiveLocationStoreTest { + private val preferenceDataStoreFactory = FakePreferenceDataStoreFactory() + + @Test + fun `disclaimer defaults to false`() = runTest { + val store = LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = A_SESSION_ID, + ) + + assertThat(store.hasAcceptedLiveLocationDisclaimer()).isFalse() + } + + @Test + fun `disclaimer acceptance is isolated per session`() = runTest { + val firstStore = LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = A_SESSION_ID, + ) + val secondStore = LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = SessionId("@other:server"), + ) + + firstStore.setAcceptedLiveLocationDisclaimer().getOrThrow() + + assertThat(firstStore.hasAcceptedLiveLocationDisclaimer()).isTrue() + assertThat(secondStore.hasAcceptedLiveLocationDisclaimer()).isFalse() + } + + @Test + fun `can persist and read expiry per room`() = runTest { + val store = LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = A_SESSION_ID, + ) + + store.setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(1_000L)).getOrThrow() + + assertThat(store.getLiveLocationExpiries()) + .containsExactly(A_ROOM_ID, Instant.fromEpochMilliseconds(1_000L)) + } + + @Test + fun `removing one expiry leaves others untouched`() = runTest { + val otherRoomId = RoomId("!other:server") + val store = LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = A_SESSION_ID, + ) + + store.setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(1_000L)).getOrThrow() + store.setLiveLocationExpiry(otherRoomId, Instant.fromEpochMilliseconds(2_000L)).getOrThrow() + store.removeLiveLocationExpiry(A_ROOM_ID).getOrThrow() + + assertThat(store.getLiveLocationExpiries()) + .containsExactly(otherRoomId, Instant.fromEpochMilliseconds(2_000L)) + } + + @Test + fun `setting expiry twice replaces the existing room value`() = runTest { + val store = LiveLocationStore( + preferenceDataStoreFactory = preferenceDataStoreFactory, + sessionId = A_SESSION_ID, + ) + + store.setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(1_000L)).getOrThrow() + store.setLiveLocationExpiry(A_ROOM_ID, Instant.fromEpochMilliseconds(2_000L)).getOrThrow() + + assertThat(store.getLiveLocationExpiries()) + .containsExactly(A_ROOM_ID, Instant.fromEpochMilliseconds(2_000L)) + } + + @Test + fun `malformed expiry payload returns empty map`() = runTest { + val store = LiveLocationStore( + preferenceDataStoreFactory = createMalformedExpiryPreferenceDataStoreFactory(), + sessionId = A_SESSION_ID, + ) + + assertThat(store.getLiveLocationExpiries()).isEmpty() + } + + private fun createMalformedExpiryPreferenceDataStoreFactory(): PreferenceDataStoreFactory { + return object : PreferenceDataStoreFactory { + override fun create(name: String): DataStore { + var preferences: Preferences = mutablePreferencesOf( + stringPreferencesKey("live_location_expiries") to "not valid" + ) + return object : DataStore { + override val data: Flow + get() = flowOf(preferences) + + override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences { + preferences = transform(preferences) + return preferences + } + } + } + } + } +} diff --git a/features/location/test/build.gradle.kts b/features/location/test/build.gradle.kts index f84e8ba772..e51737d40c 100644 --- a/features/location/test/build.gradle.kts +++ b/features/location/test/build.gradle.kts @@ -16,7 +16,7 @@ android { dependencies { api(projects.features.location.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) - implementation(libs.appyx.core) implementation(projects.tests.testutils) } diff --git a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeActiveLiveLocationShareManager.kt b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeActiveLiveLocationShareManager.kt new file mode 100644 index 0000000000..255c181ac1 --- /dev/null +++ b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeActiveLiveLocationShareManager.kt @@ -0,0 +1,46 @@ +/* + * 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.location.test + +import io.element.android.features.location.api.live.ActiveLiveLocationShareManager +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlin.time.Duration + +class FakeActiveLiveLocationShareManager( + val setupLambda: () -> Unit = { lambdaError() }, + val startShareLambda: (roomId: RoomId, duration: Duration) -> Result = { _, _ -> lambdaError() }, + val stopShareLambda: (roomId: RoomId) -> Result = { _ -> lambdaError() }, +) : ActiveLiveLocationShareManager { + private val _sharingRoomIds = MutableStateFlow(emptySet()) + override val sharingRoomIds: StateFlow> = _sharingRoomIds + + override suspend fun setup() { + setupLambda() + } + + override suspend fun startShare(roomId: RoomId, duration: Duration): Result = simulateLongTask { + startShareLambda(roomId, duration).onSuccess { + _sharingRoomIds.update { + it + roomId + } + } + } + + override suspend fun stopShare(roomId: RoomId): Result = simulateLongTask { + stopShareLambda(roomId).onSuccess { + _sharingRoomIds.update { + it - roomId + } + } + } +} diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/DeviceUnlockEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/DeviceUnlockEntryPoint.kt new file mode 100644 index 0000000000..d67668caf1 --- /dev/null +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/DeviceUnlockEntryPoint.kt @@ -0,0 +1,35 @@ +/* + * 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.lockscreen.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint + +/** + * An entry point for features that want to lock the screen and require + * the user to unlock it before they can interact with the app. + * - if the system lock is available, it will be used to unlock the screen. + * - if the system lock is not available, but app lock is available, it will be used to unlock the screen. + * - if neither is available, the screen will be unlocked immediately. + * + * The Node provided by [createNode] has to be added as a PermanentChild. + */ +interface DeviceUnlockEntryPoint : FeatureEntryPoint { + fun createNode( + parentNode: Node, + buildContext: BuildContext, + ): Node + + fun requestUnlock(callback: Callback) + + interface Callback { + fun onCancel() + fun onUnlock() + } +} diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 63aec590de..414b4b5909 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.androidx.biometric) testCommonDependencies(libs, true) + testImplementation(projects.features.lockscreen.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultDeviceUnlockEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultDeviceUnlockEntryPoint.kt new file mode 100644 index 0000000000..f4b78968f8 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultDeviceUnlockEntryPoint.kt @@ -0,0 +1,35 @@ +/* + * 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.lockscreen.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.lockscreen.api.DeviceUnlockEntryPoint +import io.element.android.features.lockscreen.impl.device.DeviceUnlockCallbackHolder +import io.element.android.features.lockscreen.impl.device.DeviceUnlockNode +import io.element.android.libraries.architecture.createNode + +@ContributesBinding(AppScope::class) +class DefaultDeviceUnlockEntryPoint( + private val deviceUnlockCallbackHolder: DeviceUnlockCallbackHolder, +) : DeviceUnlockEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + ): Node { + return parentNode.createNode( + buildContext = buildContext, + ) + } + + override fun requestUnlock(callback: DeviceUnlockEntryPoint.Callback) { + deviceUnlockCallbackHolder.requestUnlock(callback) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index 7ac42feda6..49d299b3f9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -43,7 +43,7 @@ class DefaultLockScreenService( private val coroutineScope: CoroutineScope, private val sessionObserver: SessionObserver, private val appForegroundStateService: AppForegroundStateService, - biometricAuthenticatorManager: BiometricAuthenticatorManager, + private val biometricAuthenticatorManager: BiometricAuthenticatorManager, ) : LockScreenService { private val _lockState = MutableStateFlow(LockScreenLockState.Unlocked) override val lockState: StateFlow = _lockState @@ -81,6 +81,7 @@ class DefaultLockScreenService( override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) { if (wasLastSession) { pinCodeManager.deletePinCode() + biometricAuthenticatorManager.disable() } } }) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt index a96c713ff2..74d36b0834 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticator.kt @@ -36,13 +36,13 @@ interface BiometricAuthenticator { } val isActive: Boolean - fun setup() + suspend fun setup() suspend fun authenticate(): AuthenticationResult } class NoopBiometricAuthentication : BiometricAuthenticator { override val isActive: Boolean = false - override fun setup() = Unit + override suspend fun setup() = Unit override suspend fun authenticate() = BiometricAuthenticator.AuthenticationResult.Failure() } @@ -58,7 +58,8 @@ class DefaultBiometricAuthentication( private var cryptoObject: CryptoObject? = null - override fun setup() { + override suspend fun setup() { + if (cryptoObject != null) return try { val secretKey = ensureKey() val cipher = encryptionDecryptionService.createEncryptionCipher(secretKey) @@ -86,7 +87,7 @@ class DefaultBiometricAuthentication( } @Throws(KeyPermanentlyInvalidatedException::class) - private fun ensureKey() = secretKeyRepository.getOrCreateKey(keyAlias, true).also { + private suspend fun ensureKey() = secretKeyRepository.getOrCreateKey(keyAlias, true).also { encryptionDecryptionService.createEncryptionCipher(it) } } @@ -109,7 +110,9 @@ private class AuthenticationCallback( override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) - if (result.cryptoObject?.cipher.isValid()) { + if (result.authenticationType == BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC && + result.cryptoObject?.cipher.isValid() || + result.authenticationType == BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL) { callbacks.forEach { it.onBiometricAuthenticationSuccess() } deferredAuthenticationResult.complete(BiometricAuthenticator.AuthenticationResult.Success) } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt index 9917845725..cccfa143b9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricAuthenticatorManager.kt @@ -21,15 +21,31 @@ interface BiometricAuthenticatorManager { */ val hasAvailableAuthenticator: Boolean + /** + * If the device is secured for example with a pin, a pattern, a password, or biometric. + */ + val canUseDeviceUnlock: Boolean + fun addCallback(callback: BiometricAuthenticator.Callback) fun removeCallback(callback: BiometricAuthenticator.Callback) + /** + * Disable using the biometric unlock feature and remove any data associated with it. + */ + suspend fun disable() + /** * Remember a biometric authenticator ready for unlocking the app. */ @Composable fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator + /** + * Remember a biometric authenticator ready for unlocking the app, using the device settings. + */ + @Composable + fun rememberUnlockDeviceBiometricAuthenticator(): BiometricAuthenticator + /** * Remember a biometric authenticator ready for confirmation. */ diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt index 8bb044fd06..80ea43b44d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricAuthenticatorManager.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList @@ -69,6 +70,12 @@ class DefaultBiometricAuthenticatorManager( get() = lockScreenConfig.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS + /** + * Returns true if a device credential method (i.e.: pattern, pin) can be used. + */ + private val canUseDeviceCredentialAuth: Boolean + get() = biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS + /** * Returns true if any biometric method (weak or strong) can be used. */ @@ -78,12 +85,12 @@ class DefaultBiometricAuthenticatorManager( override val isDeviceSecured: Boolean get() = keyguardManager.isDeviceSecure + override val canUseDeviceUnlock: Boolean + get() = isDeviceSecured && (canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialAuth) + private val internalCallback = object : DefaultBiometricUnlockCallback() { override fun onBiometricSetupError() { - coroutineScope.launch { - lockScreenStore.setIsBiometricUnlockAllowed(false) - secretKeyRepository.deleteKey(SECRET_KEY_ALIAS) - } + coroutineScope.launch { disable() } } } @@ -102,6 +109,28 @@ class DefaultBiometricAuthenticatorManager( isAvailable = isAvailable, promptTitle = promptTitle, promptNegative = promptNegative, + forDeviceUnlock = false, + ) + } + + @Composable + override fun rememberUnlockDeviceBiometricAuthenticator(): BiometricAuthenticator { + val isAvailableTrigger by remember { + // Need to trigger the creation of BiometricAuthenticator twice, else the callback will not be ready. + // (the issue already exists in [rememberUnlockBiometricAuthenticator]) + flowOf(false, true) + }.collectAsState(initial = false) + val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState() + val isAvailable by remember(lifecycleState) { + derivedStateOf { isAvailableTrigger && canUseDeviceUnlock } + } + val promptTitle = stringResource(id = R.string.screen_app_lock_biometric_unlock_title_android) + val promptNegative = null + return rememberBiometricAuthenticator( + isAvailable = isAvailable, + promptTitle = promptTitle, + promptNegative = promptNegative, + forDeviceUnlock = true, ) } @@ -117,14 +146,21 @@ class DefaultBiometricAuthenticatorManager( isAvailable = isAvailable, promptTitle = promptTitle, promptNegative = promptNegative, + forDeviceUnlock = false, ) } + override suspend fun disable() { + lockScreenStore.setIsBiometricUnlockAllowed(false) + secretKeyRepository.deleteKey(SECRET_KEY_ALIAS) + } + @Composable private fun rememberBiometricAuthenticator( isAvailable: Boolean, promptTitle: String, - promptNegative: String, + promptNegative: String?, + forDeviceUnlock: Boolean, ): BiometricAuthenticator { val activity = LocalContext.current.findFragmentActivity() return remember(isAvailable) { @@ -134,11 +170,21 @@ class DefaultBiometricAuthenticatorManager( canUseWeakBiometricAuth -> BiometricManager.Authenticators.BIOMETRIC_WEAK else -> 0 } - val promptInfo = BiometricPrompt.PromptInfo.Builder().apply { - setTitle(promptTitle) - setNegativeButtonText(promptNegative) - setAllowedAuthenticators(authenticators) - }.build() + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(promptTitle) + .apply { + if (promptNegative != null) { + setNegativeButtonText(promptNegative) + } + if (forDeviceUnlock) { + setAllowedAuthenticators( + authenticators or if (canUseDeviceCredentialAuth) BiometricManager.Authenticators.DEVICE_CREDENTIAL else 0 + ) + } else { + setAllowedAuthenticators(authenticators) + } + } + .build() DefaultBiometricAuthentication( activity = activity, promptInfo = promptInfo, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockCallbackHolder.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockCallbackHolder.kt new file mode 100644 index 0000000000..cf8b23d3f6 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockCallbackHolder.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.lockscreen.impl.device + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.features.lockscreen.api.DeviceUnlockEntryPoint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +@Inject +@SingleIn(AppScope::class) +class DeviceUnlockCallbackHolder { + private val _deviceUnlockCallback = MutableStateFlow(null) + val deviceUnlockCallback: StateFlow = _deviceUnlockCallback + + fun requestUnlock(callback: DeviceUnlockEntryPoint.Callback) { + _deviceUnlockCallback.value = callback + } + + fun onDone() { + _deviceUnlockCallback.value = null + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockEvent.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockEvent.kt new file mode 100644 index 0000000000..8b894875cb --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockEvent.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.lockscreen.impl.device + +sealed interface DeviceUnlockEvent { + data object CancelPinCode : DeviceUnlockEvent +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockNode.kt new file mode 100644 index 0000000000..9b73239725 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockNode.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.lockscreen.impl.device + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter +import io.element.android.features.lockscreen.impl.unlock.PinUnlockView +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +@AssistedInject +class DeviceUnlockNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: DeviceUnlockPresenter, + private val pinUnlockPresenterFactory: PinUnlockPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + if (state.showApplicationPinCode) { + val pinUnlockPresenter = remember { + pinUnlockPresenterFactory.create(forDeviceUnlock = true) + } + val pinState = pinUnlockPresenter.present() + PinUnlockView( + state = pinState, + isInAppUnlock = true, + onCancel = { + state.eventSink(DeviceUnlockEvent.CancelPinCode) + }, + modifier = modifier, + ) + } + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockPresenter.kt new file mode 100644 index 0000000000..676340764e --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockPresenter.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.lockscreen.impl.device + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.unlock.PinUnlockHelper +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.flow.first + +@Inject +class DeviceUnlockPresenter( + private val pinUnlockHelper: PinUnlockHelper, + private val biometricAuthenticatorManager: BiometricAuthenticatorManager, + private val deviceUnlockCallbackHolder: DeviceUnlockCallbackHolder, + private val pinCodeManager: PinCodeManager, +) : Presenter { + @Composable + override fun present(): DeviceUnlockState { + var showApplicationPinCode by remember { + mutableStateOf(false) + } + + val biometricUnlock = biometricAuthenticatorManager.rememberUnlockDeviceBiometricAuthenticator() + val deviceUnlockCallback by deviceUnlockCallbackHolder.deviceUnlockCallback.collectAsState() + val canUseDeviceUnlock = biometricAuthenticatorManager.canUseDeviceUnlock + + fun setUnlock(isUnlock: Boolean) { + deviceUnlockCallback?.let { + if (isUnlock) { + it.onUnlock() + } else { + it.onCancel() + } + } + showApplicationPinCode = false + deviceUnlockCallbackHolder.onDone() + } + + LaunchedEffect(biometricUnlock, canUseDeviceUnlock, deviceUnlockCallback) { + if (deviceUnlockCallback != null) { + if (canUseDeviceUnlock) { + biometricUnlock.setup() + biometricUnlock.authenticate() + } else if (pinCodeManager.hasPinCode().first()) { + showApplicationPinCode = true + } else { + // No security, unlock immediately + setUnlock(true) + } + } + } + + pinUnlockHelper.OnUnlockEffect { isUnlock -> + setUnlock(isUnlock) + } + + fun handleEvent(event: DeviceUnlockEvent) { + when (event) { + DeviceUnlockEvent.CancelPinCode -> { + showApplicationPinCode = false + setUnlock(false) + } + } + } + + return DeviceUnlockState( + showApplicationPinCode = showApplicationPinCode, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockState.kt new file mode 100644 index 0000000000..48d2181605 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockState.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.lockscreen.impl.device + +data class DeviceUnlockState( + val showApplicationPinCode: Boolean, + val eventSink: (DeviceUnlockEvent) -> Unit, +) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index 091432044a..d699357933 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -16,9 +16,13 @@ import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.cryptography.api.SecretKeyRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.util.concurrent.CopyOnWriteArrayList -private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE" +internal const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE" @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) @@ -29,6 +33,8 @@ class DefaultPinCodeManager( ) : PinCodeManager { private val callbacks = CopyOnWriteArrayList() + private val migrationMutex = Mutex() + override fun addCallback(callback: PinCodeManager.Callback) { callbacks.add(callback) } @@ -38,11 +44,20 @@ class DefaultPinCodeManager( } override fun hasPinCode(): Flow { - return lockScreenStore.hasPinCode() + return secretKeyRepository.hasKey(SECRET_KEY_ALIAS) + .onStart { + migrationMutex.withLock { + val hasKey = secretKeyRepository.hasKey(SECRET_KEY_ALIAS).first() + if (hasKey && lockScreenStore.getEncryptedCode() == null) { + // Remove the key if there is no pin code + secretKeyRepository.deleteKey(SECRET_KEY_ALIAS) + } + } + } } - override suspend fun getPinCodeSize(): Int { - val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return 0 + override suspend fun getPinCodeSize(): Int? { + val encryptedPinCode = lockScreenStore.getEncryptedCode() ?: return null val secretKey = secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, false) val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) return decryptedPinCode.size @@ -79,6 +94,7 @@ class DefaultPinCodeManager( override suspend fun deletePinCode() { lockScreenStore.deleteEncryptedPinCode() lockScreenStore.resetCounter() + secretKeyRepository.deleteKey(SECRET_KEY_ALIAS) callbacks.forEach { it.onPinCodeRemoved() } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 9282f3e7df..350631a233 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -51,9 +51,9 @@ interface PinCodeManager { fun hasPinCode(): Flow /** - * @return the size of the saved pin code. + * @return the size of the saved pin code. Return null if no pin code is saved. */ - suspend fun getPinCodeSize(): Int + suspend fun getPinCodeSize(): Int? /** * Creates a new encrypted pin code. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvent.kt similarity index 62% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvent.kt index 2d62427e02..c7437912eb 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvent.kt @@ -8,9 +8,9 @@ package io.element.android.features.lockscreen.impl.settings -sealed interface LockScreenSettingsEvents { - data object OnRemovePin : LockScreenSettingsEvents - data object ConfirmRemovePin : LockScreenSettingsEvents - data object CancelRemovePin : LockScreenSettingsEvents - data object ToggleBiometricAllowed : LockScreenSettingsEvents +sealed interface LockScreenSettingsEvent { + data object OnRemovePin : LockScreenSettingsEvent + data object ConfirmRemovePin : LockScreenSettingsEvent + data object CancelRemovePin : LockScreenSettingsEvent + data object ToggleBiometricAllowed : LockScreenSettingsEvent } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index 0bd26729d5..a6e2c1265c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -103,8 +103,15 @@ class LockScreenSettingsFlowNode( override fun onUnlock() { backstack.newRoot(NavTarget.Settings) } + + override fun onCancel() { + // Should not happen in this context. + } } - createNode(buildContext, plugins = listOf(callback)) + val inputs = PinUnlockNode.Inputs( + forDeviceUnlock = false, + ) + createNode(buildContext, plugins = listOf(callback, inputs)) } NavTarget.SetupPin -> { createNode(buildContext) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index 589794bde0..17a0213f63 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -51,19 +51,20 @@ class LockScreenSettingsPresenter( val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator() - fun handleEvent(event: LockScreenSettingsEvents) { + fun handleEvent(event: LockScreenSettingsEvent) { when (event) { - LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false - LockScreenSettingsEvents.ConfirmRemovePin -> { + LockScreenSettingsEvent.CancelRemovePin -> showRemovePinConfirmation = false + LockScreenSettingsEvent.ConfirmRemovePin -> { coroutineScope.launch { if (showRemovePinConfirmation) { showRemovePinConfirmation = false pinCodeManager.deletePinCode() + biometricAuthenticatorManager.disable() } } } - LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true - LockScreenSettingsEvents.ToggleBiometricAllowed -> { + LockScreenSettingsEvent.OnRemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvent.ToggleBiometricAllowed -> { coroutineScope.launch { if (!isBiometricEnabled) { biometricUnlock.setup() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt index a69d633508..62b8d6d4ee 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt @@ -13,5 +13,5 @@ data class LockScreenSettingsState( val isBiometricEnabled: Boolean, val showRemovePinConfirmation: Boolean, val showToggleBiometric: Boolean, - val eventSink: (LockScreenSettingsEvents) -> Unit + val eventSink: (LockScreenSettingsEvent) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index fe5f20da0d..e78a5ee002 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -51,7 +51,7 @@ fun LockScreenSettingsView( }, style = ListItemStyle.Destructive, onClick = { - state.eventSink(LockScreenSettingsEvents.OnRemovePin) + state.eventSink(LockScreenSettingsEvent.OnRemovePin) } ) } @@ -61,7 +61,7 @@ fun LockScreenSettingsView( title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock), isChecked = state.isBiometricEnabled, onCheckedChange = { - state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) } ) } @@ -72,10 +72,10 @@ fun LockScreenSettingsView( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_title), content = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_message), onSubmitClick = { - state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + state.eventSink(LockScreenSettingsEvent.ConfirmRemovePin) }, onDismiss = { - state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + state.eventSink(LockScreenSettingsEvent.CancelRemovePin) } ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvent.kt similarity index 68% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvent.kt index ab8b18642e..d4db46b731 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.lockscreen.impl.setup.biometric -sealed interface SetupBiometricEvents { - data object AllowBiometric : SetupBiometricEvents - data object UsePin : SetupBiometricEvents +sealed interface SetupBiometricEvent { + data object AllowBiometric : SetupBiometricEvent + data object UsePin : SetupBiometricEvent } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt index 3af2a28851..ce914320bc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt @@ -35,16 +35,16 @@ class SetupBiometricPresenter( val coroutineScope = rememberCoroutineScope() val biometricUnlock = biometricAuthenticatorManager.rememberConfirmBiometricAuthenticator() - fun handleEvent(event: SetupBiometricEvents) { + fun handleEvent(event: SetupBiometricEvent) { when (event) { - SetupBiometricEvents.AllowBiometric -> coroutineScope.launch { + SetupBiometricEvent.AllowBiometric -> coroutineScope.launch { biometricUnlock.setup() if (biometricUnlock.authenticate() == BiometricAuthenticator.AuthenticationResult.Success) { lockScreenStore.setIsBiometricUnlockAllowed(true) isBiometricSetupDone = true } } - SetupBiometricEvents.UsePin -> coroutineScope.launch { + SetupBiometricEvent.UsePin -> coroutineScope.launch { lockScreenStore.setIsBiometricUnlockAllowed(false) isBiometricSetupDone = true } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt index 2843c028d1..db11b1dc30 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt @@ -10,5 +10,5 @@ package io.element.android.features.lockscreen.impl.setup.biometric data class SetupBiometricState( val isBiometricSetupDone: Boolean, - val eventSink: (SetupBiometricEvents) -> Unit + val eventSink: (SetupBiometricEvent) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt index 35b1ec76c0..70a1046e36 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt @@ -33,7 +33,7 @@ fun SetupBiometricView( modifier: Modifier = Modifier, ) { BackHandler { - state.eventSink(SetupBiometricEvents.UsePin) + state.eventSink(SetupBiometricEvent.UsePin) } HeaderFooterPage( modifier = modifier.padding(top = 80.dp), @@ -42,8 +42,8 @@ fun SetupBiometricView( }, footer = { SetupBiometricFooter( - onAllowClick = { state.eventSink(SetupBiometricEvents.AllowBiometric) }, - onSkipClick = { state.eventSink(SetupBiometricEvents.UsePin) } + onAllowClick = { state.eventSink(SetupBiometricEvent.AllowBiometric) }, + onSkipClick = { state.eventSink(SetupBiometricEvent.UsePin) } ) }, ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvent.kt similarity index 74% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvent.kt index 276a94b2fc..f0dfdc33f0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.lockscreen.impl.setup.pin -sealed interface SetupPinEvents { - data class OnPinEntryChanged(val entryAsText: String, val fromConfirmationStep: Boolean) : SetupPinEvents - data object ClearFailure : SetupPinEvents +sealed interface SetupPinEvent { + data class OnPinEntryChanged(val entryAsText: String, val fromConfirmationStep: Boolean) : SetupPinEvent + data object ClearFailure : SetupPinEvent } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt index ac5b5bd1cc..d780927d44 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenter.kt @@ -74,9 +74,9 @@ class SetupPinPresenter( } } - fun handleEvent(event: SetupPinEvents) { + fun handleEvent(event: SetupPinEvent) { when (event) { - is SetupPinEvents.OnPinEntryChanged -> { + is SetupPinEvent.OnPinEntryChanged -> { // Use the fromConfirmationStep flag from ui to avoid race condition. if (event.fromConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) @@ -84,7 +84,7 @@ class SetupPinPresenter( choosePinEntry = choosePinEntry.fillWith(event.entryAsText) } } - SetupPinEvents.ClearFailure -> { + SetupPinEvent.ClearFailure -> { when (setupPinFailure) { is SetupPinFailure.PinsDoNotMatch -> { choosePinEntry = choosePinEntry.clear() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt index 2d5124d440..cf65e63c1b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinState.kt @@ -17,7 +17,7 @@ data class SetupPinState( val isConfirmationStep: Boolean, val setupPinFailure: SetupPinFailure?, val appName: String, - val eventSink: (SetupPinEvents) -> Unit + val eventSink: (SetupPinEvent) -> Unit ) { val activePinEntry = if (isConfirmationStep) { confirmPinEntry diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt index 5f2320db32..508d3c1fbb 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt @@ -107,7 +107,7 @@ private fun SetupPinContent( pinEntry = state.activePinEntry, isSecured = true, onValueChange = { entry -> - state.eventSink(SetupPinEvents.OnPinEntryChanged(entry, state.isConfirmationStep)) + state.eventSink(SetupPinEvent.OnPinEntryChanged(entry, state.isConfirmationStep)) }, modifier = Modifier .focusRequester(focusRequester) @@ -119,7 +119,7 @@ private fun SetupPinContent( title = state.setupPinFailure.title(), content = state.setupPinFailure.content(), onSubmit = { - state.eventSink(SetupPinEvents.ClearFailure) + state.eventSink(SetupPinEvent.ClearFailure) } ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt index c4558812de..b41e6a9578 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/EncryptedPinCodeStorage.kt @@ -8,8 +8,6 @@ package io.element.android.features.lockscreen.impl.storage -import kotlinx.coroutines.flow.Flow - /** * Should be implemented by any class that provides access to the encrypted PIN code. * All methods are suspending in case there are async IO operations involved. @@ -29,9 +27,4 @@ interface EncryptedPinCodeStorage { * Deletes the PIN code from some persistable storage. */ suspend fun deleteEncryptedPinCode() - - /** - * Returns whether the PIN code is stored or not. - */ - fun hasPinCode(): Flow } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt index 6b99d90592..bce20b2418 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt @@ -70,12 +70,6 @@ class PreferencesLockScreenStore( } } - override fun hasPinCode(): Flow { - return dataStore.data.map { preferences -> - preferences[pinCodeKey] != null - } - } - override fun isBiometricUnlockAllowed(): Flow { return dataStore.data.map { preferences -> preferences[biometricUnlockKey] ?: false @@ -88,5 +82,7 @@ class PreferencesLockScreenStore( } } - private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout + private fun Preferences.getRemainingPinCodeAttemptsNumber() = + this[remainingAttemptsKey]?.coerceIn(0, lockScreenConfig.maxPinCodeAttemptsBeforeLogout) + ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvent.kt similarity index 61% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvent.kt index bd9043859f..aa96a2e115 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvent.kt @@ -10,12 +10,12 @@ package io.element.android.features.lockscreen.impl.unlock import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel -sealed interface PinUnlockEvents { - data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents - data class OnPinEntryChanged(val entryAsText: String) : PinUnlockEvents - data object OnForgetPin : PinUnlockEvents - data object ClearSignOutPrompt : PinUnlockEvents - data object SignOut : PinUnlockEvents - data object OnUseBiometric : PinUnlockEvents - data object ClearBiometricError : PinUnlockEvents +sealed interface PinUnlockEvent { + data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvent + data class OnPinEntryChanged(val entryAsText: String) : PinUnlockEvent + data object OnForgetPin : PinUnlockEvent + data object ClearSignOutPrompt : PinUnlockEvent + data object SignOut : PinUnlockEvent + data object OnUseBiometric : PinUnlockEvent + data object ClearBiometricError : PinUnlockEvent } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt index 2a475814c9..10de1f23a5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockHelper.kt @@ -24,17 +24,23 @@ class PinUnlockHelper( private val pinCodeManager: PinCodeManager ) { @Composable - fun OnUnlockEffect(onUnlock: () -> Unit) { + fun OnUnlockEffect(onUnlock: (Boolean) -> Unit) { val latestOnUnlock by rememberUpdatedState(onUnlock) DisposableEffect(Unit) { val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() { override fun onBiometricAuthenticationSuccess() { - latestOnUnlock() + latestOnUnlock(true) + } + + override fun onBiometricAuthenticationFailed(error: Exception?) { + if (error != null) { + latestOnUnlock(false) + } } } val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeVerified() { - latestOnUnlock() + latestOnUnlock(true) } } biometricAuthenticatorManager.addCallback(biometricUnlockCallback) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index 459e165e00..47e7715ea5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -17,7 +17,9 @@ 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.NodeInputs import io.element.android.libraries.architecture.callback +import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -25,13 +27,23 @@ import io.element.android.libraries.di.SessionScope class PinUnlockNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: PinUnlockPresenter, + presenterFactory: PinUnlockPresenter.Factory, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun onUnlock() + + // For feature unlock + fun onCancel() } + data class Inputs( + val forDeviceUnlock: Boolean, + ) : NodeInputs + private val callback: Callback = callback() + private val inputs: Inputs = inputs() + + private val presenter: PinUnlockPresenter = presenterFactory.create(inputs.forDeviceUnlock) @Composable override fun View(modifier: Modifier) { @@ -46,6 +58,7 @@ class PinUnlockNode( // UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true. // It's set to false in PinUnlockActivity. isInAppUnlock = true, + onCancel = callback::onCancel, modifier = modifier ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 5429320fc7..65afba669d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -16,7 +16,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -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.lockscreen.impl.biometric.BiometricAuthenticator import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager @@ -32,8 +34,9 @@ import io.element.android.libraries.di.annotations.AppCoroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -@Inject +@AssistedInject class PinUnlockPresenter( + @Assisted private val forDeviceUnlock: Boolean, private val pinCodeManager: PinCodeManager, private val biometricAuthenticatorManager: BiometricAuthenticatorManager, private val logoutUseCase: LogoutUseCase, @@ -41,6 +44,11 @@ class PinUnlockPresenter( private val coroutineScope: CoroutineScope, private val pinUnlockHelper: PinUnlockHelper, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(forDeviceUnlock: Boolean): PinUnlockPresenter + } + @Composable override fun present(): PinUnlockState { val pinEntryState = remember { @@ -69,7 +77,13 @@ class PinUnlockPresenter( LaunchedEffect(Unit) { suspend { val pinCodeSize = pinCodeManager.getPinCodeSize() - PinEntry.createEmpty(pinCodeSize) + if (pinCodeSize == null) { + // No pin code set, deleted store? Force sign out + showSignOutPrompt = true + error("No pin code size found") + } else { + PinEntry.createEmpty(pinCodeSize) + } }.runCatchingUpdatingState(pinEntryState) } LaunchedEffect(biometricUnlock) { @@ -92,36 +106,37 @@ class PinUnlockPresenter( } } pinUnlockHelper.OnUnlockEffect { - isUnlocked.value = true + isUnlocked.value = it } - fun handleEvent(event: PinUnlockEvents) { + fun handleEvent(event: PinUnlockEvent) { when (event) { - is PinUnlockEvents.OnPinKeypadPressed -> { + is PinUnlockEvent.OnPinKeypadPressed -> { pinEntryState.value = pinEntry.process(event.pinKeypadModel) } - PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true - PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false - PinUnlockEvents.SignOut -> { + PinUnlockEvent.OnForgetPin -> showSignOutPrompt = true + PinUnlockEvent.ClearSignOutPrompt -> showSignOutPrompt = false + PinUnlockEvent.SignOut -> { if (showSignOutPrompt) { showSignOutPrompt = false coroutineScope.signOut(signOutAction) } } - PinUnlockEvents.OnUseBiometric -> { + PinUnlockEvent.OnUseBiometric -> { coroutineScope.launch { biometricUnlockResult = biometricUnlock.authenticate() } } - PinUnlockEvents.ClearBiometricError -> { + PinUnlockEvent.ClearBiometricError -> { biometricUnlockResult = null } - is PinUnlockEvents.OnPinEntryChanged -> { + is PinUnlockEvent.OnPinEntryChanged -> { pinEntryState.value = pinEntry.process(event.entryAsText) } } } return PinUnlockState( + canNavigateBack = forDeviceUnlock, pinEntry = pinEntry, showWrongPinTitle = showWrongPinTitle, remainingAttempts = remainingAttempts, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 2bbcbe335c..1aae6afa51 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData data class PinUnlockState( + val canNavigateBack: Boolean, val pinEntry: AsyncData, val showWrongPinTitle: Boolean, val remainingAttempts: AsyncData, @@ -23,11 +24,15 @@ data class PinUnlockState( val showBiometricUnlock: Boolean, val isUnlocked: Boolean, val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?, - val eventSink: (PinUnlockEvents) -> Unit + val eventSink: (PinUnlockEvent) -> Unit ) { - val isSignOutPromptCancellable = when (remainingAttempts) { - is AsyncData.Success -> remainingAttempts.data > 0 - else -> true + val isSignOutPromptCancellable = if (pinEntry.isFailure()) { + false + } else { + when (remainingAttempts) { + is AsyncData.Success -> remainingAttempts.data > 0 + else -> true + } } val biometricUnlockErrorMessage = when { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 2beb8babe3..aef2c057a2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -20,7 +20,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), - aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")), + aPinUnlockState(pinEntry = AsyncData.Success(PinEntry.createEmpty(4).fillWith("12"))), aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showBiometricUnlock = false), @@ -31,11 +31,28 @@ open class PinUnlockStateProvider : PreviewParameterProvider { BiometricUnlockError(BiometricPrompt.ERROR_LOCKOUT, "Biometric auth disabled") ) ), + aPinUnlockState(showSignOutPrompt = true, pinEntry = AsyncData.Failure(Exception("An error occurred"))), + // User enter wrong pin once, and then correct PIN. In this case, the error (with counter reset to 3) should not be displayed. + aPinUnlockState( + remainingAttempts = AsyncData.Success(2), + showWrongPinTitle = true, + isUnlocked = true, + ), + aPinUnlockState(canNavigateBack = true), + ) +} + +open class PinUnlockStateCompactProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPinUnlockState(), + aPinUnlockState(canNavigateBack = true) ) } fun aPinUnlockState( - pinEntry: PinEntry = PinEntry.createEmpty(4), + canNavigateBack: Boolean = false, + pinEntry: AsyncData = AsyncData.Success(PinEntry.createEmpty(4)), remainingAttempts: AsyncData = AsyncData.Success(3), showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, @@ -44,7 +61,8 @@ fun aPinUnlockState( isUnlocked: Boolean = false, signOutAction: AsyncAction = AsyncAction.Uninitialized, ) = PinUnlockState( - pinEntry = AsyncData.Success(pinEntry), + canNavigateBack = canNavigateBack, + pinEntry = pinEntry, showWrongPinTitle = showWrongPinTitle, remainingAttempts = remainingAttempts, showSignOutPrompt = showSignOutPrompt, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 659f8c2966..0292b29a0f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -50,6 +51,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview @@ -65,21 +67,22 @@ import io.element.android.libraries.ui.strings.CommonStrings fun PinUnlockView( state: PinUnlockState, isInAppUnlock: Boolean, + onCancel: () -> Unit, modifier: Modifier = Modifier, ) { OnLifecycleEvent { _, event -> when (event) { - Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvents.OnUseBiometric) + Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(PinUnlockEvent.OnUseBiometric) else -> Unit } } Surface(modifier) { - PinUnlockPage(state = state, isInAppUnlock = isInAppUnlock) + PinUnlockPage(state = state, isInAppUnlock = isInAppUnlock, onCancel = onCancel) if (state.showSignOutPrompt) { SignOutPrompt( isCancellable = state.isSignOutPromptCancellable, - onSignOut = { state.eventSink(PinUnlockEvents.SignOut) }, - onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) }, + onSignOut = { state.eventSink(PinUnlockEvent.SignOut) }, + onDismiss = { state.eventSink(PinUnlockEvent.ClearSignOutPrompt) }, ) } when (state.signOutAction) { @@ -95,7 +98,7 @@ fun PinUnlockView( if (state.showBiometricUnlockError) { ErrorDialog( content = state.biometricUnlockErrorMessage ?: "", - onSubmit = { state.eventSink(PinUnlockEvents.ClearBiometricError) } + onSubmit = { state.eventSink(PinUnlockEvent.ClearBiometricError) } ) } } @@ -105,6 +108,7 @@ fun PinUnlockView( private fun PinUnlockPage( state: PinUnlockState, isInAppUnlock: Boolean, + onCancel: () -> Unit, ) { BoxWithConstraints { val commonModifier = Modifier @@ -125,10 +129,10 @@ private fun PinUnlockPage( modifier = Modifier.padding(top = 24.dp), showBiometricUnlock = state.showBiometricUnlock, onUseBiometric = { - state.eventSink(PinUnlockEvents.OnUseBiometric) + state.eventSink(PinUnlockEvent.OnUseBiometric) }, onForgotPin = { - state.eventSink(PinUnlockEvents.OnForgetPin) + state.eventSink(PinUnlockEvent.OnForgetPin) }, ) } @@ -144,7 +148,7 @@ private fun PinUnlockPage( pinEntry = pinEntry, isSecured = true, onValueChange = { - state.eventSink(PinUnlockEvents.OnPinEntryChanged(it)) + state.eventSink(PinUnlockEvent.OnPinEntryChanged(it)) }, modifier = Modifier .focusRequester(focusRequester) @@ -154,7 +158,7 @@ private fun PinUnlockPage( } else { PinKeypad( onClick = { - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(it)) }, maxWidth = constraints.maxWidth, maxHeight = constraints.maxHeight, @@ -177,6 +181,15 @@ private fun PinUnlockPage( modifier = commonModifier, ) } + if (state.canNavigateBack) { + BackButton( + onClick = onCancel, + modifier = Modifier + .align(Alignment.TopStart) + .systemBarsPadding() + .padding(8.dp), + ) + } } } @@ -311,14 +324,26 @@ private fun PinUnlockHeader( ) Spacer(Modifier.height(8.dp)) val remainingAttempts = state.remainingAttempts.dataOrNull() - val subtitle = if (remainingAttempts != null) { - if (state.showWrongPinTitle) { - pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts) - } else { - pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts) + val subtitle = when { + state.isUnlocked -> { + // Hide any previous error + "" } - } else { - "" + remainingAttempts != null -> + if (state.showWrongPinTitle) { + pluralStringResource( + id = R.plurals.screen_app_lock_subtitle_wrong_pin, + count = remainingAttempts, + remainingAttempts, + ) + } else { + pluralStringResource( + id = R.plurals.screen_app_lock_subtitle, + count = remainingAttempts, + remainingAttempts, + ) + } + else -> "" } val subtitleColor = if (state.showWrongPinTitle) { ElementTheme.colors.textCriticalPrimary @@ -361,6 +386,7 @@ internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider: PinUnlockView( state = state, isInAppUnlock = true, + onCancel = {}, ) } } @@ -372,6 +398,19 @@ internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::clas PinUnlockView( state = state, isInAppUnlock = false, + onCancel = {}, + ) + } +} + +@Composable +@Preview(heightDp = 480, widthDp = 800) +internal fun PinUnlockViewCompactPreview(@PreviewParameter(PinUnlockStateCompactProvider::class) state: PinUnlockState) { + ElementPreview { + PinUnlockView( + state = state, + isInAppUnlock = false, + onCancel = {}, ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt index 34adfeca99..613b1cf79e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt @@ -41,7 +41,7 @@ class PinUnlockActivity : AppCompatActivity() { } } - @Inject lateinit var presenter: PinUnlockPresenter + @Inject lateinit var presenterFactory: PinUnlockPresenter.Factory @Inject lateinit var lockScreenService: LockScreenService @Inject lateinit var appPreferencesStore: AppPreferencesStore @Inject lateinit var featureFlagService: FeatureFlagService @@ -52,6 +52,7 @@ class PinUnlockActivity : AppCompatActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) bindings().inject(this) + val presenter = presenterFactory.create(forDeviceUnlock = false) setContent { val colors by remember { enterpriseService.semanticColorsFlow(sessionId = null) @@ -67,6 +68,9 @@ class PinUnlockActivity : AppCompatActivity() { PinUnlockView( state = state, isInAppUnlock = false, + onCancel = { + // Should not happen + }, ) } } diff --git a/features/lockscreen/impl/src/main/res/values-be/translations.xml b/features/lockscreen/impl/src/main/res/values-be/translations.xml index 7667034623..7b714d329d 100644 --- a/features/lockscreen/impl/src/main/res/values-be/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-be/translations.xml @@ -35,5 +35,5 @@ "Выкарыстоўваць біяметрыю" "Выкарыстоўваць PIN-код" - "Выхад…" + "Выдаленне прылады…" diff --git a/features/lockscreen/impl/src/main/res/values-ca/translations.xml b/features/lockscreen/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..5637977b97 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,38 @@ + + + "l\'autenticació biomètrica" + "desbloqueig biomètric" + "Desbloqueja amb biometria" + "Confirma biometria" + "Has oblidat el PIN?" + "Canvia codi PIN" + "Permet desbloqueig biomètric" + "Elimina PIN" + "Segur que vols eliminar el PIN?" + "Vols eliminar el PIN?" + "Permet %1$s" + "Prefereixo utilitzar el PIN" + "Estalvia\'t temps i utilitza %1$s per desbloquejar l\'aplicació" + "Escull el PIN" + "Confirma PIN" + "Bloqueja %1$s per afegir més seguretat als teus xats. + +Escull alguna cosa que recordis. Si oblides aquest PIN, es tancarà sessió a l\'aplicació." + "Per motius de seguretat no pots utilitzar aquest codi PIN" + "Escull un PIN diferent" + "Introdueix el mateix PIN dues vegades" + "Els codis PIN no coincideixen" + "Hauràs de tornar a iniciar sessió i crear un nou PIN per continuar." + "S\'està tancant la sessió" + + "Tens %1$d intent per desbloquejar" + "Tens %1$d intents per desbloquejar" + + + "PIN incorrecte. Tens %1$d intent més" + "PIN incorrecte. Tens %1$d intents més" + + "Utilitza biometria" + "Utilitza PIN" + "S\'està tancant la sessió…" + diff --git a/features/lockscreen/impl/src/main/res/values-et/translations.xml b/features/lockscreen/impl/src/main/res/values-et/translations.xml index ac25914f8c..7137c09b60 100644 --- a/features/lockscreen/impl/src/main/res/values-et/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-et/translations.xml @@ -34,5 +34,5 @@ Vali midagi, mis hästi meelde jääb. Kui unustad selle PIN-koodi, siis turvaka "Kasuta biomeetriat" "Kasuta PIN-koodi" - "Logime välja…" + "Eemaldan seadet…" diff --git a/features/lockscreen/impl/src/main/res/values-fa/translations.xml b/features/lockscreen/impl/src/main/res/values-fa/translations.xml index 56dc91e835..0575f22221 100644 --- a/features/lockscreen/impl/src/main/res/values-fa/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-fa/translations.xml @@ -23,7 +23,7 @@ "لطفاً یک پین را دو بار وارد کنید" "پین‌ها مطابق نیستند" "برای ادامه باید دوباره وارد شده و پینی جدید ایجاد کنید" - "دارید خارج می‌شوید" + "این دستگاه در حال حذف شدن است" "شما %1$d تلاش برای باز کردن قفل دارید" "شما %1$d تلاش برای باز کردن قفل دارید" @@ -34,5 +34,5 @@ "استفاده از زیست‌سنجی" "استفاده از پین" - "خارج شدن…" + "برداشتن افزاره…" diff --git a/features/lockscreen/impl/src/main/res/values-in/translations.xml b/features/lockscreen/impl/src/main/res/values-in/translations.xml index 0396f56b0c..e0054cda62 100644 --- a/features/lockscreen/impl/src/main/res/values-in/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-in/translations.xml @@ -32,5 +32,5 @@ Pilih sesuatu yang mudah untuk diingat. Jika Anda lupa PIN ini, Anda akan dikelu "Gunakan biometrik" "Gunakan PIN" - "Mengeluarkan dari akun…" + "Mengeluarkan device dari akun…" diff --git a/features/lockscreen/impl/src/main/res/values-pl/translations.xml b/features/lockscreen/impl/src/main/res/values-pl/translations.xml index 5d61ecb7c6..29691987f6 100644 --- a/features/lockscreen/impl/src/main/res/values-pl/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-pl/translations.xml @@ -23,7 +23,7 @@ Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz ten PIN, zostaniesz wyl "Wprowadź ten sam kod PIN dwa razy" "PIN\'y nie pasują do siebie" "Aby kontynuować, zaloguj się ponownie i utwórz nowy kod PIN" - "Trwa wylogowywanie" + "Trwa usuwanie urządzenia" "Masz %1$d próbę, żeby odblokować" "Masz %1$d próby, żeby odblokować" @@ -36,5 +36,5 @@ Wybierz coś łatwego do zapamiętania. Jeśli zapomnisz ten PIN, zostaniesz wyl "Użyj biometrii" "Użyj kodu PIN" - "Wylogowywanie…" + "Usuwam urządzenie…" diff --git a/features/lockscreen/impl/src/main/res/values-pt/translations.xml b/features/lockscreen/impl/src/main/res/values-pt/translations.xml index a6b2516fba..fca6fbcb9e 100644 --- a/features/lockscreen/impl/src/main/res/values-pt/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-pt/translations.xml @@ -23,7 +23,7 @@ Escolhe algo memorável. Se te esqueceres deste PIN, a tua sessão será termina "Insere o mesmo PIN duas vezes" "Os PINs não coincidem" "Terás de voltar a iniciar sessão e criar um novo PIN para continuar" - "Estás a terminar a sessão" + "O teu dispositivo está a ser removido" "Tens %1$d tentativa de desbloqueio" "Tens %1$d tentativas de desbloqueio" @@ -34,5 +34,5 @@ Escolhe algo memorável. Se te esqueceres deste PIN, a tua sessão será termina "Utilizar biometria" "Utilizar PIN" - "A terminar sessão…" + "A remover dispositivo…" 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 cb71dfc88c..7bbdef1594 100644 --- a/features/lockscreen/impl/src/main/res/values-ru/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml @@ -15,9 +15,9 @@ "Используйте %1$s для разблокировки приложения" "Выберите PIN-код" "Подтвердите PIN-код" - "Установите блокировку на %1$s, чтобы повысить безопасность ваших чатов. + "Установите блокировку на %1$s, чтобы повысить безопасность Ваших чатов. -Выберите код, который трудно забыть. Если вы забудете PIN-код, вам придется выйти из приложения." +Выберите код, который трудно забыть. Если Вы забудете PIN-код, Вам придется выйти из аккаунта." "Этот PIN-код небезопасен." "Выбрать другой PIN-код" "Повторите PIN-код" @@ -25,14 +25,14 @@ "Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код" "Это устройство удаляется" - "У вас осталась %1$d попытка разблокировки" - "У вас остались %1$d попытки разблокировки" - "У вас осталось %1$d попыток разблокировки" + "У Вас осталась %1$d попытка разблокировки" + "У Вас остались %1$d попытки разблокировки" + "У Вас осталось %1$d попыток разблокировки" - "Неверный PIN-код. У вас осталась %1$d попытка" - "Неверный PIN-код. У вас остались %1$d попытки" - "Неверный PIN-код. У вас осталось %1$d попыток" + "Неверный PIN-код. У Вас осталась %1$d попытка" + "Неверный PIN-код. У Вас остались %1$d попытки" + "Неверный PIN-код. У Вас осталось %1$d попыток" "Использовать биометрию" "Использовать PIN-код" diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml index 0cfb2e88cd..e8fdf50bfd 100644 --- a/features/lockscreen/impl/src/main/res/values-sk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -23,7 +23,7 @@ Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplik "Zadajte prosím ten istý PIN dvakrát" "PIN kódy sa nezhodujú" "Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód." - "Prebieha odhlasovanie" + "Toto zariadenie sa odstraňuje" "Máte %1$d pokus na odomknutie" "Máte %1$d pokusy na odomknutie" @@ -36,5 +36,5 @@ Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplik "Použiť biometrické údaje" "Použiť PIN" - "Prebieha odhlasovanie…" + "Odoberanie zariadenia…" diff --git a/features/lockscreen/impl/src/main/res/values-uk/translations.xml b/features/lockscreen/impl/src/main/res/values-uk/translations.xml index 5c19889282..25e96003c8 100644 --- a/features/lockscreen/impl/src/main/res/values-uk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-uk/translations.xml @@ -36,5 +36,5 @@ "Використати біометрію" "Використати PIN-код" - "Вихід…" + "Видалення пристрою…" 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 62da77cbb5..f3e93668fd 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/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt index e99223735f..e2b410e663 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPointIntentTest.kt @@ -8,15 +8,13 @@ package io.element.android.features.lockscreen.impl -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultLockScreenEntryPointIntentTest { +class DefaultLockScreenEntryPointIntentTest : RobolectricTest() { @Test fun `test pin unlock intent`() { val entryPoint = DefaultLockScreenEntryPoint() diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt index 9082f20a55..f906d0d6ba 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenServiceTest.kt @@ -14,9 +14,12 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricAuthentica import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.SECRET_KEY_ALIAS import io.element.android.features.lockscreen.impl.pin.createDefaultPinCodeManager import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore import io.element.android.features.lockscreen.impl.storage.LockScreenStore +import io.element.android.libraries.cryptography.api.SecretKeyRepository +import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver import io.element.android.services.appnavstate.api.AppForegroundStateService @@ -38,18 +41,18 @@ class DefaultLockScreenServiceTest { @Test fun `when the pin is mandatory, isSetupRequired emits true`() = runTest { - val lockScreenStore = InMemoryLockScreenStore() + val secretKeyRepository = SimpleSecretKeyRepository() val sut = createDefaultLockScreenService( lockScreenConfig = aLockScreenConfig(isPinMandatory = true), - lockScreenStore = lockScreenStore, + secretKeyRepository = secretKeyRepository, ) sut.isSetupRequired().test { assertThat(awaitItem()).isTrue() // When the user configures the pin code, the setup is not required anymore - lockScreenStore.saveEncryptedPinCode("encryptedCode") + secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, true) assertThat(awaitItem()).isFalse() // Users deletes the pin code - lockScreenStore.deleteEncryptedPinCode() + secretKeyRepository.deleteKey("elementx.SECRET_KEY_ALIAS_PIN_CODE") assertThat(awaitItem()).isTrue() } } @@ -57,16 +60,16 @@ class DefaultLockScreenServiceTest { @Test fun `when the last session is deleted, the pin code is removed`() = runTest { val sessionObserver = FakeSessionObserver() - val lockScreenStore = InMemoryLockScreenStore() + val secretKeyRepository = SimpleSecretKeyRepository() val sut = createDefaultLockScreenService( lockScreenConfig = aLockScreenConfig(isPinMandatory = true), - lockScreenStore = lockScreenStore, + secretKeyRepository = secretKeyRepository, sessionObserver = sessionObserver, ) sut.isPinSetup().test { assertThat(awaitItem()).isFalse() // When the user configure the pin code, the setup is not required anymore - lockScreenStore.saveEncryptedPinCode("encryptedCode") + secretKeyRepository.getOrCreateKey(SECRET_KEY_ALIAS, true) assertThat(awaitItem()).isTrue() sessionObserver.onSessionDeleted("userId", wasLastSession = false) expectNoEvents() @@ -79,8 +82,10 @@ class DefaultLockScreenServiceTest { private fun TestScope.createDefaultLockScreenService( lockScreenConfig: LockScreenConfig = aLockScreenConfig(), lockScreenStore: LockScreenStore = InMemoryLockScreenStore(), + secretKeyRepository: SecretKeyRepository = SimpleSecretKeyRepository(), pinCodeManager: PinCodeManager = createDefaultPinCodeManager( lockScreenStore = lockScreenStore, + secretKeyRepository = secretKeyRepository, ), sessionObserver: SessionObserver = FakeSessionObserver(), appForegroundStateService: AppForegroundStateService = FakeAppForegroundStateService(), diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt index 073bdc799d..0fb2e9df8b 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticator.kt @@ -8,10 +8,18 @@ package io.element.android.features.lockscreen.impl.biometric +import io.element.android.tests.testutils.simulateLongTask + class FakeBiometricAuthenticator( override val isActive: Boolean = false, - private val authenticateLambda: suspend () -> BiometricAuthenticator.AuthenticationResult = { BiometricAuthenticator.AuthenticationResult.Success }, + private val setupLambda: () -> Unit = { }, + private val authenticateLambda: () -> BiometricAuthenticator.AuthenticationResult = { BiometricAuthenticator.AuthenticationResult.Success }, ) : BiometricAuthenticator { - override fun setup() = Unit - override suspend fun authenticate() = authenticateLambda() + override suspend fun setup() = simulateLongTask { + setupLambda() + } + + override suspend fun authenticate() = simulateLongTask { + authenticateLambda() + } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt index 9e9b892582..1a70f25d30 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricAuthenticatorManager.kt @@ -12,9 +12,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember class FakeBiometricAuthenticatorManager( - override var isDeviceSecured: Boolean = true, - override var hasAvailableAuthenticator: Boolean = false, + override val isDeviceSecured: Boolean = true, + override val canUseDeviceUnlock: Boolean = true, + override val hasAvailableAuthenticator: Boolean = false, private val createBiometricAuthenticator: () -> BiometricAuthenticator = { FakeBiometricAuthenticator() }, + private val disableLambda: suspend () -> Unit = { }, ) : BiometricAuthenticatorManager { override fun addCallback(callback: BiometricAuthenticator.Callback) { // no-op @@ -27,7 +29,14 @@ class FakeBiometricAuthenticatorManager( @Composable override fun rememberUnlockBiometricAuthenticator(): BiometricAuthenticator { return remember { - createBiometricAuthenticator() + createBiometricAuthenticator() + } + } + + @Composable + override fun rememberUnlockDeviceBiometricAuthenticator(): BiometricAuthenticator { + return remember { + createBiometricAuthenticator() } } @@ -37,4 +46,8 @@ class FakeBiometricAuthenticatorManager( createBiometricAuthenticator() } } + + override suspend fun disable() { + disableLambda() + } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockPresenterTest.kt new file mode 100644 index 0000000000..52aeaa04a8 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/device/DeviceUnlockPresenterTest.kt @@ -0,0 +1,152 @@ +/* + * 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.lockscreen.impl.device + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator +import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticator +import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.unlock.PinUnlockHelper +import io.element.android.features.lockscreen.test.FakeDeviceUnlockEntryPointCallback +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DeviceUnlockPresenterTest { + @Test + fun `present - when unlock requested and device unlock available, use biometric authenticator`() = runTest { + val setupLambda = lambdaRecorder { } + val authenticateLambda = lambdaRecorder { + BiometricAuthenticator.AuthenticationResult.Success + } + val fakeBiometricAuthenticator = FakeBiometricAuthenticator( + setupLambda = setupLambda, + authenticateLambda = authenticateLambda, + ) + val biometricAuthenticatorManager = FakeBiometricAuthenticatorManager( + canUseDeviceUnlock = true, + createBiometricAuthenticator = { fakeBiometricAuthenticator }, + ) + val callbackHolder = DeviceUnlockCallbackHolder() + val callback = FakeDeviceUnlockEntryPointCallback() + + createDeviceUnlockPresenter( + biometricAuthenticatorManager = biometricAuthenticatorManager, + callbackHolder = callbackHolder, + ).test { + awaitItem().also { state -> + assertThat(state.showApplicationPinCode).isFalse() + } + callbackHolder.requestUnlock(callback) + advanceUntilIdle() + setupLambda.assertions().isCalledOnce() + authenticateLambda.assertions().isCalledOnce() + skipItems(1) + } + } + + @Test + fun `present - when unlock requested and device unlock unavailable and app pin is configured, show app pin`() = runTest { + val callbackHolder = DeviceUnlockCallbackHolder() + val callback = FakeDeviceUnlockEntryPointCallback() + val pinCodeManager = aPinCodeManager().apply { + createPinCode("1234") + } + createDeviceUnlockPresenter( + biometricAuthenticatorManager = FakeBiometricAuthenticatorManager(canUseDeviceUnlock = false), + callbackHolder = callbackHolder, + pinCodeManager = pinCodeManager, + ).test { + awaitItem().also { state -> + assertThat(state.showApplicationPinCode).isFalse() + } + + callbackHolder.requestUnlock(callback) + skipItems(1) + awaitItem().also { state -> + assertThat(state.showApplicationPinCode).isTrue() + } + } + } + + @Test + fun `present - when unlock requested and no security, unlock immediately`() = runTest { + val callbackHolder = DeviceUnlockCallbackHolder() + val onUnlockedLambda = lambdaRecorder { } + val callback = FakeDeviceUnlockEntryPointCallback( + onUnlockedLambda = onUnlockedLambda, + ) + createDeviceUnlockPresenter( + biometricAuthenticatorManager = FakeBiometricAuthenticatorManager(canUseDeviceUnlock = false), + callbackHolder = callbackHolder, + ).test { + awaitItem().also { state -> + assertThat(state.showApplicationPinCode).isFalse() + } + callbackHolder.requestUnlock(callback) + skipItems(2) + assertThat(callbackHolder.deviceUnlockCallback.value).isNull() + onUnlockedLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - CancelPinCode event cancels unlock request`() = runTest { + val callbackHolder = DeviceUnlockCallbackHolder() + val onCancelLambda = lambdaRecorder { } + val callback = FakeDeviceUnlockEntryPointCallback( + onCancelLambda = onCancelLambda, + ) + val pinCodeManager = aPinCodeManager().apply { + createPinCode("1234") + } + createDeviceUnlockPresenter( + biometricAuthenticatorManager = FakeBiometricAuthenticatorManager(canUseDeviceUnlock = false), + callbackHolder = callbackHolder, + pinCodeManager = pinCodeManager, + ).test { + awaitItem() + callbackHolder.requestUnlock(callback) + skipItems(1) + awaitItem().also { state -> + assertThat(state.showApplicationPinCode).isTrue() + state.eventSink(DeviceUnlockEvent.CancelPinCode) + } + awaitItem().also { state -> + assertThat(state.showApplicationPinCode).isFalse() + } + skipItems(1) + onCancelLambda.assertions().isCalledOnce() + assertThat(callbackHolder.deviceUnlockCallback.value).isNull() + } + } + + private fun createDeviceUnlockPresenter( + biometricAuthenticatorManager: FakeBiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(), + callbackHolder: DeviceUnlockCallbackHolder = DeviceUnlockCallbackHolder(), + pinCodeManager: PinCodeManager = aPinCodeManager(), + ): DeviceUnlockPresenter { + val pinUnlockHelper = PinUnlockHelper( + biometricAuthenticatorManager = biometricAuthenticatorManager, + pinCodeManager = pinCodeManager, + ) + return DeviceUnlockPresenter( + pinUnlockHelper = pinUnlockHelper, + biometricAuthenticatorManager = biometricAuthenticatorManager, + deviceUnlockCallbackHolder = callbackHolder, + pinCodeManager = pinCodeManager, + ) + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt index 61acf71cdd..312a33b7f1 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryLockScreenStore.kt @@ -15,12 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow private const val DEFAULT_REMAINING_ATTEMPTS = 3 class InMemoryLockScreenStore : LockScreenStore { - private val hasPinCode = MutableStateFlow(false) private var pinCode: String? = null - set(value) { - field = value - hasPinCode.value = value != null - } private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS private var isBiometricUnlockAllowed = MutableStateFlow(false) @@ -48,10 +43,6 @@ class InMemoryLockScreenStore : LockScreenStore { pinCode = null } - override fun hasPinCode(): Flow { - return hasPinCode - } - override fun isBiometricUnlockAllowed(): Flow { return isBiometricUnlockAllowed } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt index ef3e94f27f..85eb4a37e7 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt @@ -43,19 +43,19 @@ class LockScreenSettingsPresenterTest { consumeItemsUntilPredicate { state -> state.showRemovePinOption }.last().also { state -> - state.eventSink(LockScreenSettingsEvents.OnRemovePin) + state.eventSink(LockScreenSettingsEvent.OnRemovePin) } awaitLastSequentialItem().also { state -> assertThat(state.showRemovePinConfirmation).isTrue() - state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + state.eventSink(LockScreenSettingsEvent.CancelRemovePin) } awaitLastSequentialItem().also { state -> assertThat(state.showRemovePinConfirmation).isFalse() - state.eventSink(LockScreenSettingsEvents.OnRemovePin) + state.eventSink(LockScreenSettingsEvent.OnRemovePin) } awaitLastSequentialItem().also { state -> assertThat(state.showRemovePinConfirmation).isTrue() - state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + state.eventSink(LockScreenSettingsEvent.ConfirmRemovePin) } consumeItemsUntilPredicate { it.showRemovePinOption.not() @@ -93,7 +93,7 @@ class LockScreenSettingsPresenterTest { presenter.test { skipItems(1) awaitItem().also { state -> - state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) } awaitItem().also { state -> assertThat(state.isBiometricEnabled).isTrue() @@ -114,7 +114,7 @@ class LockScreenSettingsPresenterTest { presenter.test { skipItems(1) awaitItem().also { state -> - state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) } } } @@ -137,7 +137,7 @@ class LockScreenSettingsPresenterTest { skipItems(1) awaitItem().also { state -> assertThat(state.isBiometricEnabled).isTrue() - state.eventSink(LockScreenSettingsEvents.ToggleBiometricAllowed) + state.eventSink(LockScreenSettingsEvent.ToggleBiometricAllowed) } awaitItem().also { state -> assertThat(state.isBiometricEnabled).isFalse() diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt index 3f87c1dccf..9dde220906 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt @@ -8,9 +8,6 @@ package io.element.android.features.lockscreen.impl.setup.biometric -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticator import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager @@ -18,6 +15,7 @@ import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthen import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore import io.element.android.features.lockscreen.impl.storage.LockScreenStore +import io.element.android.tests.testutils.test import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test @@ -30,12 +28,10 @@ class SetupBiometricPresenterTest { FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Success }) }) val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isFalse() - state.eventSink(SetupBiometricEvents.AllowBiometric) + state.eventSink(SetupBiometricEvent.AllowBiometric) } awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isTrue() @@ -51,12 +47,10 @@ class SetupBiometricPresenterTest { FakeBiometricAuthenticator(authenticateLambda = { BiometricAuthenticator.AuthenticationResult.Failure() }) }) val presenter = createSetupBiometricPresenter(lockScreenStore, fakeBiometricAuthenticatorManager) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isFalse() - state.eventSink(SetupBiometricEvents.AllowBiometric) + state.eventSink(SetupBiometricEvent.AllowBiometric) } } assertThat(lockScreenStore.isBiometricUnlockAllowed().first()).isFalse() @@ -66,12 +60,10 @@ class SetupBiometricPresenterTest { fun `present - skip flow`() = runTest { val lockScreenStore = InMemoryLockScreenStore() val presenter = createSetupBiometricPresenter(lockScreenStore) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isFalse() - state.eventSink(SetupBiometricEvents.UsePin) + state.eventSink(SetupBiometricEvent.UsePin) } awaitItem().also { state -> assertThat(state.isBiometricSetupDone).isTrue() diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt index 6a1d32e879..9d63f9e26b 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinPresenterTest.kt @@ -8,9 +8,6 @@ package io.element.android.features.lockscreen.impl.setup.pin -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.LockScreenConfig import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig @@ -24,6 +21,7 @@ import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPin import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.test import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Test @@ -43,9 +41,7 @@ class SetupPinPresenterTest { } } val presenter = createSetupPinPresenter(callback) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() @@ -63,7 +59,7 @@ class SetupPinPresenterTest { awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(forbiddenPin) assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.ForbiddenPin) - state.eventSink(SetupPinEvents.ClearFailure) + state.eventSink(SetupPinEvent.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() @@ -82,7 +78,7 @@ class SetupPinPresenterTest { state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(mismatchedPin) assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinsDoNotMatch) - state.eventSink(SetupPinEvents.ClearFailure) + state.eventSink(SetupPinEvent.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() @@ -108,7 +104,7 @@ class SetupPinPresenterTest { } private fun SetupPinState.onPinEntryChanged(pinEntry: String) { - eventSink(SetupPinEvents.OnPinEntryChanged(pinEntry, isConfirmationStep)) + eventSink(SetupPinEvent.OnPinEntryChanged(pinEntry, isConfirmationStep)) } private fun createSetupPinPresenter( diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index f5bfb11818..b20a99d27f 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -8,9 +8,6 @@ package io.element.android.features.lockscreen.impl.unlock -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager @@ -19,12 +16,14 @@ import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCall import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText +import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -36,9 +35,7 @@ class PinUnlockPresenterTest { @Test fun `present - success verify flow`() = runTest { val presenter = createPinUnlockPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(state.showWrongPinTitle).isFalse() @@ -50,17 +47,17 @@ class PinUnlockPresenterTest { awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } skipItems(1) awaitItem().also { state -> state.pinEntry.assertText(halfCompletePin) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Back)) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Empty)) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('5'))) } skipItems(4) awaitItem().also { state -> @@ -73,9 +70,7 @@ class PinUnlockPresenterTest { @Test fun `present - failure verify flow`() = runTest { val presenter = createPinUnlockPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) val initialState = awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) @@ -83,10 +78,10 @@ class PinUnlockPresenterTest { } val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 repeat(numberOfAttempts) { - initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) - initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) - initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) - initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('4'))) + initialState.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + initialState.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + initialState.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + initialState.eventSink(PinUnlockEvent.OnPinKeypadPressed(PinKeypadModel.Number('4'))) } skipItems(4 * numberOfAttempts + 2) awaitItem().also { state -> @@ -102,27 +97,25 @@ class PinUnlockPresenterTest { val signOutLambda = lambdaRecorder {} val signOut = FakeLogoutUseCase(signOutLambda) val presenter = createPinUnlockPresenter(logoutUseCase = signOut) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { skipItems(1) awaitItem().also { state -> assertThat(state.pinEntry).isInstanceOf(AsyncData.Success::class.java) assertThat(state.remainingAttempts).isInstanceOf(AsyncData.Success::class.java) - state.eventSink(PinUnlockEvents.OnForgetPin) + state.eventSink(PinUnlockEvent.OnForgetPin) } awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() assertThat(state.isSignOutPromptCancellable).isTrue() - state.eventSink(PinUnlockEvents.ClearSignOutPrompt) + state.eventSink(PinUnlockEvent.ClearSignOutPrompt) } awaitItem().also { state -> assertThat(state.showSignOutPrompt).isFalse() - state.eventSink(PinUnlockEvents.OnForgetPin) + state.eventSink(PinUnlockEvent.OnForgetPin) } awaitItem().also { state -> assertThat(state.showSignOutPrompt).isTrue() - state.eventSink(PinUnlockEvents.SignOut) + state.eventSink(PinUnlockEvent.SignOut) } skipItems(2) awaitItem().also { state -> @@ -132,20 +125,70 @@ class PinUnlockPresenterTest { } } + @Test + fun `present - pin is configured, but deleted in store, sign out prompt will be shown`() = runTest { + val lockScreenStore = InMemoryLockScreenStore() + val pinCodeManager = aPinCodeManager( + lockScreenStore = lockScreenStore, + ) + val presenter = createPinUnlockPresenter( + pinCodeManager = pinCodeManager, + ) + // Delete the pin code from the store + lockScreenStore.deleteEncryptedPinCode() + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.pinEntry).isInstanceOf(AsyncData.Failure::class.java) + assertThat(state.showSignOutPrompt).isTrue() + assertThat(state.isSignOutPromptCancellable).isFalse() + assertThat(state.remainingAttempts.dataOrNull()).isEqualTo(3) + } + } + } + + @Test + fun `present - forDeviceUnlock is exposed in state`() = runTest { + val presenter = createPinUnlockPresenter(forDeviceUnlock = true) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.canNavigateBack).isTrue() + } + } + } + + @Test + fun `present - pin entry changed event updates pin entry`() = runTest { + val presenter = createPinUnlockPresenter() + presenter.test { + skipItems(1) + awaitItem().also { state -> + state.eventSink(PinUnlockEvent.OnPinEntryChanged(halfCompletePin)) + } + awaitItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + } + } + } + private fun AsyncData.assertText(text: String) { dataOrNull()?.assertText(text) } private suspend fun TestScope.createPinUnlockPresenter( + forDeviceUnlock: Boolean = false, biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), - logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = { "" }), + logoutUseCase: FakeLogoutUseCase = FakeLogoutUseCase(logoutLambda = {}), + pinCodeManager: PinCodeManager = aPinCodeManager() ): PinUnlockPresenter { - val pinCodeManager = aPinCodeManager().apply { + pinCodeManager.apply { addCallback(callback) createPinCode(completePin) } return PinUnlockPresenter( + forDeviceUnlock = forDeviceUnlock, pinCodeManager = pinCodeManager, biometricAuthenticatorManager = biometricAuthenticatorManager, logoutUseCase = logoutUseCase, diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt index 1ecb79bd67..fb7d3aa372 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadTest.kt @@ -6,60 +6,55 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.lockscreen.impl.unlock.keypad import android.view.KeyEvent import androidx.activity.ComponentActivity import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasText import androidx.compose.ui.test.isRoot -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.performKeyInput import androidx.compose.ui.test.pressKey import androidx.compose.ui.test.requestFocus +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.unit.dp import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class PinKeypadTest { - @get:Rule - val rule = createAndroidComposeRule() +class PinKeypadTest : RobolectricTest() { @Test - fun `clicking on a number emits the expected event`() { + fun `clicking on a number emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPinKeyPad(onClick = eventsRecorder) - rule.onNode(hasText("1")).performClick() + setPinKeyPad(onClick = eventsRecorder) + onNode(hasText("1")).performClick() eventsRecorder.assertSingle(PinKeypadModel.Number('1')) } @Test - fun `clicking on the delete previous character button emits the expected event`() { + fun `clicking on the delete previous character button emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPinKeyPad(onClick = eventsRecorder) - rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.a11y_delete))).performClick() + setPinKeyPad(onClick = eventsRecorder) + onNode(hasContentDescription(activity!!.getString(CommonStrings.a11y_delete))).performClick() eventsRecorder.assertSingle(PinKeypadModel.Back) } @OptIn(ExperimentalTestApi::class) @Test - fun `typing using the hardware keyboard emits the expected events`() { + fun `typing using the hardware keyboard emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPinKeyPad(onClick = eventsRecorder) - rule.onNodeWithText("1").requestFocus() - rule.onAllNodes(isRoot())[0].performKeyInput { + setPinKeyPad(onClick = eventsRecorder) + onNodeWithText("1").requestFocus() + onAllNodes(isRoot())[0].performKeyInput { val keys = listOf( Key.A, Key.NumPad1, @@ -118,7 +113,7 @@ class PinKeypadTest { ) } - private fun AndroidComposeTestRule.setPinKeyPad( + private fun AndroidComposeUiTest.setPinKeyPad( onClick: (PinKeypadModel) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeDeviceUnlockEntryPoint.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeDeviceUnlockEntryPoint.kt new file mode 100644 index 0000000000..8278fc882f --- /dev/null +++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeDeviceUnlockEntryPoint.kt @@ -0,0 +1,22 @@ +/* + * 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.lockscreen.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.lockscreen.api.DeviceUnlockEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeDeviceUnlockEntryPoint : DeviceUnlockEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + ): Node = lambdaError() + + override fun requestUnlock(callback: DeviceUnlockEntryPoint.Callback) = lambdaError() +} diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeDeviceUnlockEntryPointCallback.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeDeviceUnlockEntryPointCallback.kt new file mode 100644 index 0000000000..3b7394cae0 --- /dev/null +++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeDeviceUnlockEntryPointCallback.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.lockscreen.test + +import io.element.android.features.lockscreen.api.DeviceUnlockEntryPoint +import io.element.android.tests.testutils.EnsureNeverCalled + +class FakeDeviceUnlockEntryPointCallback( + private val onCancelLambda: () -> Unit = EnsureNeverCalled(), + private val onUnlockedLambda: () -> Unit = EnsureNeverCalled(), +) : DeviceUnlockEntryPoint.Callback { + override fun onCancel() = onCancelLambda() + override fun onUnlock() = onUnlockedLambda() +} diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 12af922cbe..86a8e0e7bc 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -60,7 +60,6 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) - implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.designsystem) @@ -69,7 +68,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.qrcode) - implementation(projects.libraries.oidc.api) + implementation(projects.libraries.oauth.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.wellknown.api) implementation(libs.androidx.browser) @@ -81,9 +80,8 @@ dependencies { 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) + testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.wellknown.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 fb384d505a..978d28dfa3 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 @@ -50,9 +50,9 @@ import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -64,7 +64,7 @@ class LoginFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val accountProviderDataSource: AccountProviderDataSource, - private val oidcActionFlow: OidcActionFlow, + private val oAuthActionFlow: OAuthActionFlow, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val elementClassicConnection: ElementClassicConnection, @@ -100,7 +100,7 @@ class LoginFlowNode( // by pressing back or by closing the Custom Chrome Tab. lifecycleScope.launch { delay(5000) - oidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) + oAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) } } } @@ -161,8 +161,8 @@ class LoginFlowNode( backstack.push(NavTarget.LoginPassword()) } - override fun navigateToOidc(oidcDetails: OidcDetails) { - navigateToMas(oidcDetails) + override fun navigateToOAuth(oAuthDetails: OAuthDetails) { + navigateToMas(oAuthDetails) } override fun navigateToCreateAccount(url: String) { @@ -197,8 +197,8 @@ class LoginFlowNode( callback.navigateToBugReport() } - override fun navigateToOidc(oidcDetails: OidcDetails) { - navigateToMas(oidcDetails) + override fun navigateToOAuth(oAuthDetails: OAuthDetails) { + navigateToMas(oAuthDetails) } override fun navigateToCreateAccount(url: String) { @@ -243,8 +243,8 @@ class LoginFlowNode( } NavTarget.ChooseAccountProvider -> { val callback = object : ChooseAccountProviderNode.Callback { - override fun navigateToOidc(oidcDetails: OidcDetails) { - navigateToMas(oidcDetails) + override fun navigateToOAuth(oAuthDetails: OAuthDetails) { + navigateToMas(oAuthDetails) } override fun navigateToCreateAccount(url: String) { @@ -270,8 +270,8 @@ class LoginFlowNode( isAccountCreation = navTarget.isAccountCreation, ) val callback = object : ConfirmAccountProviderNode.Callback { - override fun navigateToOidc(oidcDetails: OidcDetails) { - navigateToMas(oidcDetails) + override fun navigateToOAuth(oAuthDetails: OAuthDetails) { + navigateToMas(oAuthDetails) } override fun navigateToCreateAccount(url: String) { @@ -333,10 +333,10 @@ class LoginFlowNode( } } - private fun navigateToMas(oidcDetails: OidcDetails) { + private fun navigateToMas(oAuthDetails: OAuthDetails) { activity?.let { externalAppStarted = true - it.openUrlInChromeCustomTab(null, darkTheme, oidcDetails.url) + it.openUrlInChromeCustomTab(null, darkTheme, oAuthDetails.url) } } 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 index c928c05239..dfddd1d496 100644 --- 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 @@ -28,8 +28,6 @@ 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 @@ -71,7 +69,6 @@ class DefaultElementClassicConnection( 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 @@ -119,10 +116,6 @@ class DefaultElementClassicConnection( 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. @@ -158,11 +151,6 @@ class DefaultElementClassicConnection( 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") diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt index 2f4af14237..560e6123c1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt @@ -41,7 +41,7 @@ sealed class ChangeServerError : Exception() { // AccountAlreadyLoggedIn error should not happen at this point is AuthenticationException.AccountAlreadyLoggedIn -> Error(messageStr = error.message) is AuthenticationException.Generic -> Error(messageStr = error.message) - is AuthenticationException.Oidc -> Error(messageStr = error.message) + is AuthenticationException.OAuth -> Error(messageStr = error.message) } } is AccountProviderAccessException.NeedElementProException -> NeedElementPro( 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 78be770bfc..3c871a8a1d 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 @@ -23,9 +23,9 @@ import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationR import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OidcPrompt -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.matrix.api.auth.OAuthPrompt +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow /** * This class is responsible for managing the login flow, including handling OIDC actions and @@ -35,7 +35,7 @@ import io.element.android.libraries.oidc.api.OidcActionFlow */ @Inject class LoginHelper( - private val oidcActionFlow: OidcActionFlow, + private val oAuthActionFlow: OAuthActionFlow, private val authenticationService: MatrixAuthenticationService, private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, ) { @@ -44,9 +44,9 @@ class LoginHelper( @Composable fun collectLoginMode(): State> { LaunchedEffect(Unit) { - oidcActionFlow.collect { oidcAction -> - if (oidcAction != null) { - onOidcAction(oidcAction) + oAuthActionFlow.collect { oAuthAction -> + if (oAuthAction != null) { + onOAuthAction(oAuthAction) } } } @@ -73,11 +73,11 @@ class LoginHelper( throw it } }.map { matrixHomeServerDetails -> - if (matrixHomeServerDetails.supportsOidcLogin) { + if (matrixHomeServerDetails.supportsOAuthLogin) { // Retrieve the details right now - val oidcPrompt = if (isAccountCreation) OidcPrompt.Create else OidcPrompt.Login - LoginMode.Oidc( - authenticationService.getOidcUrl(prompt = oidcPrompt, loginHint = loginHint).getOrThrow() + val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login + LoginMode.OAuth( + authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow() ) } else if (isAccountCreation) { val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) @@ -99,16 +99,16 @@ class LoginHelper( ) } - private suspend fun onOidcAction(oidcAction: OidcAction) { - if (oidcAction is OidcAction.GoBack && oidcAction.toUnblock && loginModeState.value !is AsyncData.Loading) { + private suspend fun onOAuthAction(oAuthAction: OAuthAction) { + if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) { // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode. // This can happen if there is an error, for instance attempt to login again on the same account. return } loginModeState.value = AsyncData.Loading() - when (oidcAction) { - is OidcAction.GoBack -> { - authenticationService.cancelOidcLogin() + when (oAuthAction) { + is OAuthAction.GoBack -> { + authenticationService.cancelOAuthLogin() .onSuccess { loginModeState.value = AsyncData.Uninitialized } @@ -116,13 +116,13 @@ class LoginHelper( loginModeState.value = AsyncData.Failure(failure) } } - is OidcAction.Success -> { - authenticationService.loginWithOidc(oidcAction.url) + is OAuthAction.Success -> { + authenticationService.loginWithOAuth(oAuthAction.url) .onFailure { failure -> loginModeState.value = AsyncData.Failure(failure) } } } - oidcActionFlow.reset() + oAuthActionFlow.reset() } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt index 08e604ef20..5ea52e0ebd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt @@ -8,10 +8,10 @@ package io.element.android.features.login.impl.login -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails sealed interface LoginMode { data object PasswordLogin : LoginMode - data class Oidc(val oidcDetails: OidcDetails) : LoginMode + data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode data class AccountCreation(val url: String) : LoginMode } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt index f88e34bf4a..3549e17457 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.matrix.api.auth.AuthenticationException -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -32,7 +32,7 @@ fun LoginModeView( loginMode: AsyncData, onClearError: () -> Unit, onLearnMoreClick: () -> Unit, - onOidcDetails: (OidcDetails) -> Unit, + onOAuthDetails: (OAuthDetails) -> Unit, onNeedLoginPassword: () -> Unit, onCreateAccountContinue: (url: String) -> Unit ) { @@ -118,7 +118,7 @@ fun LoginModeView( is AsyncData.Loading -> Unit // The Continue button shows the loading state is AsyncData.Success -> { when (val loginModeData = loginMode.data) { - is LoginMode.Oidc -> onOidcDetails(loginModeData.oidcDetails) + is LoginMode.OAuth -> onOAuthDetails(loginModeData.oAuthDetails) LoginMode.PasswordLogin -> onNeedLoginPassword() is LoginMode.AccountCreation -> onCreateAccountContinue(loginModeData.url) } @@ -137,7 +137,7 @@ internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider:: loginMode = AsyncData.Failure(error), onClearError = {}, onLearnMoreClick = {}, - onOidcDetails = {}, + onOAuthDetails = {}, onNeedLoginPassword = {}, onCreateAccountContinue = {} ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt index 613aa6aeb6..03264551a5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNode.kt @@ -135,8 +135,8 @@ class QrCodeLoginFlowNode( is QrLoginException.SlidingSyncNotAvailable -> { backstack.replace(NavTarget.Error(QrCodeErrorScreenType.SlidingSyncNotAvailable)) } - is QrLoginException.OidcMetadataInvalid -> { - Timber.e(error, "OIDC metadata is invalid") + is QrLoginException.OAuthMetadataInvalid -> { + Timber.e(error, "OAuth metadata is invalid") backstack.replace(NavTarget.Error(QrCodeErrorScreenType.UnknownError)) } QrLoginException.CheckCodeAlreadySent, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt index 5dc6ebbd6b..5f79f197d9 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderNode.kt @@ -20,7 +20,7 @@ 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.callback -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails @ContributesNode(AppScope::class) @AssistedInject @@ -31,7 +31,7 @@ class ChooseAccountProviderNode( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun navigateToLoginPassword() - fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToOAuth(oAuthDetails: OAuthDetails) fun navigateToCreateAccount(url: String) } @@ -45,7 +45,7 @@ class ChooseAccountProviderNode( state = state, modifier = modifier, onBackClick = ::navigateUp, - onOidcDetails = callback::navigateToOidc, + onOAuthDetails = callback::navigateToOAuth, 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/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index cdb80304a7..f05606dbc3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -43,14 +43,14 @@ 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.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ChooseAccountProviderView( state: ChooseAccountProviderState, onBackClick: () -> Unit, - onOidcDetails: (OidcDetails) -> Unit, + onOAuthDetails: (OAuthDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -129,7 +129,7 @@ fun ChooseAccountProviderView( state.eventSink(ChooseAccountProviderEvents.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOidcDetails = onOidcDetails, + onOAuthDetails = onOAuthDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -144,7 +144,7 @@ internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountPro state = state, onBackClick = { }, onLearnMoreClick = { }, - onOidcDetails = { }, + onOAuthDetails = { }, onNeedLoginPassword = { }, onCreateAccountContinue = { }, ) 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 index f2ff998652..cfbd86f363 100644 --- 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 @@ -31,7 +31,7 @@ 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.auth.OAuthDetails import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -54,7 +54,7 @@ class ClassicFlowNode( interface Callback : Plugin { fun navigateToOnBoarding(allowBackNavigation: Boolean) fun navigateToLoginPassword() - fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToOAuth(oAuthDetails: OAuthDetails) fun navigateToCreateAccount(url: String) } @@ -111,8 +111,8 @@ class ClassicFlowNode( callback.navigateToLoginPassword() } - override fun navigateToOidc(oidcDetails: OidcDetails) { - callback.navigateToOidc(oidcDetails) + override fun navigateToOAuth(oAuthDetails: OAuthDetails) { + callback.navigateToOAuth(oAuthDetails) } override fun navigateToCreateAccount(url: String) { 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 index c42248a3f8..d5acca38ae 100644 --- 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 @@ -21,7 +21,7 @@ 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.auth.OAuthDetails import io.element.android.libraries.matrix.api.core.UserId @ContributesNode(AppScope::class) @@ -35,7 +35,7 @@ class LoginWithClassicNode( interface Callback : Plugin { fun navigateToOtherOptions() fun navigateToLoginPassword() - fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToOAuth(oAuthDetails: OAuthDetails) fun navigateToCreateAccount(url: String) fun navigateToMissingKeyBackup() } @@ -60,7 +60,7 @@ class LoginWithClassicNode( state = state, modifier = modifier, onOtherOptionsClick = callback::navigateToOtherOptions, - onOidcDetails = callback::navigateToOidc, + onOAuthDetails = callback::navigateToOAuth, 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/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt index d8dcfeb072..31b1770f63 100644 --- 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 @@ -12,13 +12,14 @@ 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.designsystem.preview.USER_NAME_ALICE 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"), + aLoginWithClassicState(isElementPro = true, displayName = USER_NAME_ALICE), ) } 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 index 6b5c48f1ec..b1ca50fe61 100644 --- 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 @@ -49,7 +49,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.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -59,7 +59,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LoginWithClassicView( state: LoginWithClassicState, onOtherOptionsClick: () -> Unit, - onOidcDetails: (OidcDetails) -> Unit, + onOAuthDetails: (OAuthDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -200,7 +200,7 @@ fun LoginWithClassicView( state.eventSink(LoginWithClassicEvent.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOidcDetails = onOidcDetails, + onOAuthDetails = onOAuthDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -212,7 +212,7 @@ internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicState LoginWithClassicView( state = state, onOtherOptionsClick = {}, - onOidcDetails = {}, + onOAuthDetails = {}, onNeedLoginPassword = {}, onLearnMoreClick = {}, onCreateAccountContinue = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt index e3643afbf2..928a493dc1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt @@ -22,7 +22,7 @@ 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.auth.OAuthDetails @ContributesNode(AppScope::class) @AssistedInject @@ -44,7 +44,7 @@ class ConfirmAccountProviderNode( interface Callback : Plugin { fun navigateToLoginPassword() - fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToOAuth(oAuthDetails: OAuthDetails) fun navigateToCreateAccount(url: String) fun navigateToChangeAccountProvider() } @@ -58,7 +58,7 @@ class ConfirmAccountProviderNode( ConfirmAccountProviderView( state = state, modifier = modifier, - onOidcDetails = callback::navigateToOidc, + onOAuthDetails = callback::navigateToOAuth, onNeedLoginPassword = callback::navigateToLoginPassword, onCreateAccountContinue = callback::navigateToCreateAccount, onChange = callback::navigateToChangeAccountProvider, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index a175ab556d..c2525f3756 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -30,7 +30,7 @@ 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.TextButton -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -38,7 +38,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ConfirmAccountProviderView( state: ConfirmAccountProviderState, - onOidcDetails: (OidcDetails) -> Unit, + onOAuthDetails: (OAuthDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -103,7 +103,7 @@ fun ConfirmAccountProviderView( eventSink(ConfirmAccountProviderEvents.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOidcDetails = onOidcDetails, + onOAuthDetails = onOAuthDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -117,7 +117,7 @@ internal fun ConfirmAccountProviderViewPreview( ) = ElementPreview { ConfirmAccountProviderView( state = state, - onOidcDetails = {}, + onOAuthDetails = {}, onNeedLoginPassword = {}, onCreateAccountContinue = {}, onLearnMoreClick = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index d3641ea749..feb1d3d53a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -60,6 +60,7 @@ 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.PasswordVisibilityToggle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -249,16 +250,10 @@ private fun LoginForm( placeholder = stringResource(CommonStrings.common_password), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { - val image = - if (passwordVisible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() - val description = - if (passwordVisible) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) - Box(Modifier.clickable { passwordVisible = !passwordVisible }) { - Icon( - imageVector = image, - contentDescription = description, - ) - } + PasswordVisibilityToggle( + visible = passwordVisible, + onToggle = { passwordVisible = !passwordVisible }, + ) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, 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 5572c412a0..99f7e86fd3 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 @@ -22,7 +22,7 @@ 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.auth.OAuthDetails @ContributesNode(AppScope::class) @AssistedInject @@ -40,7 +40,7 @@ class OnBoardingNode( fun navigateToQrCode() fun navigateToBugReport() fun navigateToLoginPassword() - fun navigateToOidc(oidcDetails: OidcDetails) + fun navigateToOAuth(oAuthDetails: OAuthDetails) fun navigateToCreateAccount(url: String) fun navigateToDeveloperSettings() fun onDone() @@ -71,7 +71,7 @@ class OnBoardingNode( onCreateAccount = callback::navigateToSignUpFlow, onSignInWithQrCode = callback::navigateToQrCode, onReportProblem = callback::navigateToBugReport, - onOidcDetails = callback::navigateToOidc, + onOAuthDetails = callback::navigateToOAuth, 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/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 5ee7ab6ac4..53c36ac4f8 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 @@ -50,7 +50,7 @@ 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 -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -68,7 +68,7 @@ fun OnBoardingView( onSignInWithQrCode: () -> Unit, onSignIn: (mustChooseAccountProvider: Boolean) -> Unit, onCreateAccount: () -> Unit, - onOidcDetails: (OidcDetails) -> Unit, + onOAuthDetails: (OAuthDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, onCreateAccountContinue: (url: String) -> Unit, @@ -82,7 +82,7 @@ fun OnBoardingView( state.eventSink(OnBoardingEvents.ClearError) }, onLearnMoreClick = onLearnMoreClick, - onOidcDetails = onOidcDetails, + onOAuthDetails = onOAuthDetails, onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) @@ -354,7 +354,7 @@ internal fun OnBoardingViewPreview( onSignIn = {}, onCreateAccount = {}, onReportProblem = {}, - onOidcDetails = {}, + onOAuthDetails = {}, onNeedLoginPassword = {}, onLearnMoreClick = {}, onCreateAccountContinue = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt index a6d9da3de2..6b5da8f300 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt @@ -125,7 +125,7 @@ private fun Content(errorScreenType: QrCodeErrorScreenType) { } @Composable -private fun Buttons( +private fun ColumnScope.Buttons( onRetry: () -> Unit, onCancel: () -> Unit, ) { diff --git a/features/login/impl/src/main/res/values-be/translations.xml b/features/login/impl/src/main/res/values-be/translations.xml index 307ccf7263..5018377eb0 100644 --- a/features/login/impl/src/main/res/values-be/translations.xml +++ b/features/login/impl/src/main/res/values-be/translations.xml @@ -20,15 +20,16 @@ "Які адрас вашага сервера?" "Выберыце свой сервер" "Стварыць уліковы запіс" - "Гэты ўліковы запіс быў дэактываваны." + "Гэты ўліковы запіс быў выдалены." "Няправільнае імя карыстальніка і/або пароль" "Гэта несапраўдны ідэнтыфікатар карыстальніка. Чаканы фармат: ‘@user:homeserver.org’" "Гэты сервер настроены на выкарыстанне маркераў абнаўлення. Яны не падтрымліваюцца пры ўваходзе на аснове пароля." - "Выбраны хатні сервер не падтрымлівае пароль або ўваход у OIDC. Калі ласка, звярніцеся да адміністратара або абярыце іншы хатні сервер." + "Выбраны хатні сервер не падтрымлівае пароль або ўваход у OAuth. Калі ласка, звярніцеся да адміністратара або абярыце іншы хатні сервер." "Увядзіце свае даныя" "Matrix - гэта адкрытая сетка для бяспечнай, дэцэнтралізаванай сувязі." "Сардэчна запрашаем!" "Увайсці ў %1$s" + "Версія %1$s" "Увайсці ўручную" "Увайсці ў %1$s" "Увайсці з QR-кодам" diff --git a/features/login/impl/src/main/res/values-bg/translations.xml b/features/login/impl/src/main/res/values-bg/translations.xml index 6ccc7c9129..c6c74ff1a8 100644 --- a/features/login/impl/src/main/res/values-bg/translations.xml +++ b/features/login/impl/src/main/res/values-bg/translations.xml @@ -21,7 +21,7 @@ "Този акаунт бе деактивиран." "Неправилно потребителско име и/или парола" "Това не е валиден потребителски идентификатор. Очакван формат: ‘@user:homeserver.org’" - "Избраният сървър не поддържа влизане с парола или OIDC. Моля, свържете се с вашия администратор или изберете друг сървър." + "Избраният сървър не поддържа влизане с парола или OAuth. Моля, свържете се с вашия администратор или изберете друг сървър." "Въведете своите данни" "Matrix е отворена мрежа за сигурна, децентрализирана комуникация." "Добре дошли отново!" diff --git a/features/login/impl/src/main/res/values-ca/translations.xml b/features/login/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..f079e7b071 --- /dev/null +++ b/features/login/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,89 @@ + + + "Canvia el proveïdor del compte" + "Adreça del servidor" + "Introdueix una paraula de cerca o un domini (adreça)." + "Cerca una empresa, una comunitat o un servidor privat." + "Busca un proveïdor de comptes" + "Aquí és on es guardaran els teus xats, de manera similar a utilitzar un proveïdor de correu electrònic per guardar els teus correus electrònics." + "Estàs a punt d\'iniciar sessió a %s" + "Aquí és on es guardaran els teus xats, de manera similar a utilitzar un proveïdor de correu electrònic per guardar els teus correus electrònics." + "Estàs a punt de crear un compte a %s" + "Matrix.org és un gran servidor gratuït de la xarxa pública de Matrix per a la comunicació segura i descentralitzada, gestionat per la Fundació Matrix.org." + "Altres" + "Utilitza un proveïdor de comptes diferent, com ara el teu servidor privat o un compte de feina." + "Canvia el proveïdor del compte" + "No s\'ha pogut accedir a aquest servidor. Comprova que hagis introduït correctament l\'URL del servidor. Si ja és correcte, posa\'t en contacte amb l\'administrador del servidor per a més informació." + "Servidor no disponible a causa d\'un problema al fitxer .well-known: +%1$s" + "URL del servidor" + "Introdueix un domini." + "Quina és l\'adreça del teu servidor?" + "Selecciona el teu servidor" + "Crea un compte" + "Aquest compte s\'ha desactivat." + "Usuari i/o contrasenya incorrectes" + "Identificador d\'usuari invàlid. Format esperat: ‘@usuari:servidor.org’" + "Aquest servidor està configurat per utilitzar tokens d\'actualització, però no són compatibles quan s\'utilitza l\'inici de sessió basat en contrasenya." + "El servidor seleccionat no admet contrasenya o inici de sessió OAuth. Posa\'t en contacte amb l\'administrador o tria un altre servidor." + "Introdueix les teves dades" + "Matrix és una xarxa oberta per a comunicacions segures i descentralitzades." + "Hola de nou!" + "Inicia sessió a %1$s" + "Inicia sessió manualment" + "Inicia sessió a %1$s" + "Inicia sessió amb un codi QR" + "Crea un compte" + "Et donem la benvinguda a %1$s. Més ràpid i simple que mai." + "Et donem la benvinguda a %1$s. Dissenyat per ser més ràpid i simple." + "Sigues el teu propi element" + "Establint una connexió segura" + "No s\'ha pogut establir una connexió segura amb el dispositiu nou. Els dispositius existents continuen sent segurs, no te n\'has de preocupar." + "I ara què?" + "Prova de tornar a iniciar sessió mitjançant un codi QR si es tracta d\'un problema de xarxa." + "Si es repeteix el mateix problema, prova una xarxa wifi diferent o utilitza les dades mòbils en lloc del wifi." + "Si no funciona, inicia sessió manualment" + "Connexió no segura" + "Se\'t demanarà que introdueixis els dos dígits mostrats en aquest dispositiu." + "Introdueix el número següent a l\'altre dispositiu" + "Inicia sessió a l\'altre dispositiu i torna-ho a provar o utilitza un altre dispositiu amb la sessió ja iniciada." + "No s\'ha iniciat sessió a l\'altre dispositiu" + "L\'inici de sessió s\'ha cancel·lat a l\'altre dispositiu." + "Sol·licitud d\'inici de sessió cancel·lada" + "L\'inici de sessió s\'ha rebutjat a l\'altre dispositiu." + "Inici de sessió rebutjat" + "Inici de sessió ha caducat. Torna-ho a provar." + "L\'inici de sessió no s\'ha completat a temps" + "El teu altre dispositiu no admet l\'inici de sessió a %s amb codis QR. + +Prova d\'iniciar la sessió manualment o escaneja el QR amb un altre dispositiu." + "Codi QR no compatible" + "El proveïdor del teu compte no admet %1$s." + "%1$s no és compatible" + "Preparat per escanejar" + "Obre %1$s en un dispositiu d\'escriptori" + "Clica la teva imatge" + "Selecciona %1$s" + "“Enllaça nou dispositiu”" + "Escaneja el codi QR amb aquest dispositiu" + "Només disponible si el proveïdor del compte ho admet." + "Obre %1$s en un altre dispositiu per obtenir el codi QR" + "Utilitza el codi QR que es mostra a l\'altre dispositiu." + "Torna-ho a intentar" + "Codi QR incorrecte" + "Vés a la configuració de càmera" + "Per continuar, has donar permís a %1$s per poder utilitzar la càmera del dispositiu." + "Permet l\'accés a la càmera per poder escanejar el codi QR" + "Escaneja el QR" + "Torna a començar" + "S\'ha produït un error inesperat. Torna-ho a provar." + "Esperant el teu altre dispositiu" + "Per verificar l\'inici de sessió, pot ser que el proveïdor del teu compte et demani el següent codi." + "Codi de verificació" + "Canvia el proveïdor del compte" + "Servidor privat per a treballadors d\'Element." + "Matrix és una xarxa oberta per a comunicacions segures i descentralitzades." + "Aquí és on es guardaran els teus xats, de manera similar a utilitzar un proveïdor de correu electrònic per guardar els teus correus electrònics." + "Estàs a punt d\'iniciar sessió a %1$s" + "Estàs a punt de crear un compte a %1$s" + diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index fa31796c3b..3181e823bb 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -28,11 +28,11 @@ "Jaká je adresa vašeho serveru?" "Vyberte váš server" "Vytvořit účet" - "Tento účet byl deaktivován." + "Tento účet byl smazán." "Nesprávné uživatelské jméno nebo heslo" "Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'" "Tento server je nakonfigurován tak, aby používal obnovovací tokeny. Ty nejsou podporovány při použití přihlašovacích údajů založených na hesle." - "Vybraný domovský server nepodporuje přihlášení pomocí hesla nebo OIDC. Kontaktujte prosím svého správce nebo vyberte jiný domovský server." + "Vybraný domovský server nepodporuje přihlášení pomocí hesla nebo OAuth. Kontaktujte prosím svého správce nebo vyberte jiný domovský server." "Zadejte své údaje" "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." "Vítejte zpět!" diff --git a/features/login/impl/src/main/res/values-cy/translations.xml b/features/login/impl/src/main/res/values-cy/translations.xml index b8988a9889..0f44287ed4 100644 --- a/features/login/impl/src/main/res/values-cy/translations.xml +++ b/features/login/impl/src/main/res/values-cy/translations.xml @@ -32,7 +32,7 @@ "Enw defnyddiwr a/neu gyfrinair anghywir" "Nid yw hwn yn ddynodwr defnyddiwr dilys. Fformat disgwyliedig: ‘@user:homeserver.org’" "Mae\'r gweinydd hwn wedi\'i ffurfweddu i ddefnyddio tocynnau adnewyddu. Nid yw\'r rhain yn cael eu cefnogi wrth ddefnyddio mewngofnodi ar sail cyfrinair." - "Nid yw\'r gweinydd cartref ddewiswyd yn cefnogi cyfrinair na mewngofnodi OIDC. Cysylltwch â\'ch gweinyddwr neu dewis gweinydd cartref arall." + "Nid yw\'r gweinydd cartref ddewiswyd yn cefnogi cyfrinair na mewngofnodi OAuth. Cysylltwch â\'ch gweinyddwr neu dewis gweinydd cartref arall." "Rhowch eich manylion" "Mae Matrix yn rhwydwaith agored ar gyfer cyfathrebu diogel, datganoledig." "Croeso nôl!" diff --git a/features/login/impl/src/main/res/values-da/translations.xml b/features/login/impl/src/main/res/values-da/translations.xml index 284284cc3e..35d66a6e69 100644 --- a/features/login/impl/src/main/res/values-da/translations.xml +++ b/features/login/impl/src/main/res/values-da/translations.xml @@ -28,16 +28,24 @@ "Hvad er adressen på din server?" "Vælg din server" "Opret konto" - "Denne konto er blevet deaktiveret." + "Denne konto er blevet slettet." "Forkert brugernavn og/eller adgangskode" "Dette er ikke en gyldig brugeridentifikation. Forventet format: \'@bruger:hjemmeserver.org\'" "Denne server er konfigureret til at bruge opdateringstokens. Disse understøttes ikke, når du bruger adgangskodebaseret login." - "Den valgte hjemmeserver understøtter ikke adgangskode eller OIDC-login. Kontakt venligst din administrator eller vælg en anden hjemmeserver." + "Den valgte hjemmeserver understøtter ikke adgangskode eller OAuth-login. Kontakt venligst din administrator eller vælg en anden hjemmeserver." "Indtast dine oplysninger" "Matrix er et åbent netværk for sikker, decentraliseret kommunikation." "Velkommen tilbage!" "Log ind på %1$s" + "Åbn Element Classic" + "Åbn Element Classic på din enhed" + "Gå til Indstillinger > Sikkerhed og privatliv" + "I Nøgleadministration skal du, under Kryptografi, vælge Gendannelse af krypterede meddelelser" + "Følg instruktionerne for at aktivere dit nøglelager" + "Gå tilbage til %1$s" + "Aktivér dit nøglelager, før du fortsætter til %1$s" "Version %1$s" + "Kontoen kontrolleres…" "Log ind manuelt" "Log ind på %1$s" "Log ind med QR-kode" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index dced91a1c9..89e4412d0f 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -28,11 +28,11 @@ "Wie lautet die Adresse deines Servers?" "Wähle deinen Server aus" "Konto erstellen" - "Dieses Konto wurde deaktiviert." + "Dieses Konto wurde gelöscht." "Falscher Nutzername und/oder Passwort" "Dies ist keine gültige Nutzerkennung. Erwartetes Format: \'@nutzer:homeserver.org\'" "Dieser Server ist so konfiguriert, dass er Refresh-Tokens verwendet. Diese werden für die passwortbasierte Anmeldung nicht unterstützt." - "Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OIDC. Bitte kontaktiere deinen Administrator oder wähle einen anderen Homeserver." + "Der ausgewählte Homeserver unterstützt weder den Login per Passwort noch per OAuth. Bitte kontaktiere deinen Administrator oder wähle einen anderen Homeserver." "Gib deine Daten ein" "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." "Willkommen zurück!" diff --git a/features/login/impl/src/main/res/values-el/translations.xml b/features/login/impl/src/main/res/values-el/translations.xml index c87027477d..045465902b 100644 --- a/features/login/impl/src/main/res/values-el/translations.xml +++ b/features/login/impl/src/main/res/values-el/translations.xml @@ -28,15 +28,22 @@ "Ποια είναι η διεύθυνση του διακομιστή σου;" "Επέλεξε το διακομιστή σου" "Δημιουργία λογαριασμού" - "Αυτός ο λογαριασμός έχει απενεργοποιηθεί." + "Αυτός ο λογαριασμός έχει διαγραφεί." "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης" "Αυτό δεν είναι έγκυρο αναγνωριστικό χρήστη. Αναμενόμενη μορφή: \'@χρήστης:homeserver.org\'" "Αυτός ο διακομιστής έχει ρυθμιστεί ώστε να χρησιμοποιεί διακριτικά ανανέωσης. Αυτά δεν υποστηρίζονται όταν χρησιμοποιείς σύνδεση μέσω κωδικού πρόσβασης." - "Ο επιλεγμένος οικιακός διακομιστής δεν υποστηρίζει κωδικό πρόσβασης ή σύνδεση OIDC. Επικοινωνήστε με τον διαχειριστή σου ή επέλεξε άλλο οικιακό διακομιστή." + "Ο επιλεγμένος οικιακός διακομιστής δεν υποστηρίζει κωδικό πρόσβασης ή σύνδεση OAuth. Επικοινωνήστε με τον διαχειριστή σου ή επέλεξε άλλο οικιακό διακομιστή." "Εισήγαγε τα στοιχεία σου" "Το Matrix είναι ένα ανοιχτό δίκτυο για ασφαλή, αποκεντρωμένη επικοινωνία." "Καλωσόρισες ξανά!" "Συνδέσου στο %1$s" + "Άνοιγμα του Element Classic" + "Ανοίξτε το Element Classic στη συσκευή σας." + "Μεταβείτε στις Ρυθμίσεις > Ασφάλεια και Απόρρητο" + "Στη Διαχείριση κλειδιών κρυπτογράφησης, επιλέξτε Ανάκτηση κρυπτογραφημένων μηνυμάτων" + "Ακολουθήστε τις οδηγίες για να ενεργοποιήσετε την αποθήκευση κλειδιών" + "Επιστρέψτε στο %1$s" + "Ενεργοποιήστε την αποθήκευση κλειδιών σας πριν προχωρήσετε στο %1$s" "Έκδοση %1$s" "Σύνδεση χειροκίνητα" "Συνδέσου στο %1$s" diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index df6a06ab29..3b9ffbe2dd 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -29,7 +29,7 @@ "Usuario y/o contraseña incorrectos" "Este no es un id de usuario válido. Formato esperado: \'@user:homeserver.org\'" "Este servidor está configurado para utilizar tokens de actualización. Estos no son compatibles cuando se utiliza el inicio de sesión basado en contraseña." - "El servidor base seleccionado no admite el inicio de sesión usando contraseña ni OIDC. Ponte en contacto con tu administrador o elige otro servidor base." + "El servidor base seleccionado no admite el inicio de sesión usando contraseña ni OAuth. Ponte en contacto con tu administrador o elige otro servidor base." "Introduce tus datos" "Matrix es una red abierta para una comunicación segura y descentralizada." "¡Hola de nuevo!" diff --git a/features/login/impl/src/main/res/values-et/translations.xml b/features/login/impl/src/main/res/values-et/translations.xml index 7ac4e99694..dc37ed3e38 100644 --- a/features/login/impl/src/main/res/values-et/translations.xml +++ b/features/login/impl/src/main/res/values-et/translations.xml @@ -28,18 +28,24 @@ "Mis on sinu koduserveri aadress?" "Vali oma server" "Loo kasutajakonto" - "Konto on kasutusest eemaldatud." + "See kasutajakonto on kustutatud." "Vigane kasutajanimi ja/või salasõna" "See ei ole korrektne kasutajanimi. Õige vorming on: „@kasutaja:koduserver.ee“" "See server on seadistatud kasutama tunnusloa põhist sisselogimist. Salasõnaga sisselogimisel see võimalus aga ei ole toetatud." - "Valitud koduserver ei toeta salasõna ega OIDC-põhist sisselogimist. Lisateavet saad koduserveri haldajalt, aga sa võid ka valida mõne teise serveri." + "Valitud koduserver ei toeta salasõna ega OAuth-põhist sisselogimist. Lisateavet saad koduserveri haldajalt, aga sa võid ka valida mõne teise serveri." "Sisesta oma andmed" "Matrix on avatud võrk turvalise ja hajutatud suhtluse jaoks." "Tere tulemast tagasi!" "Logi sisse serverisse %1$s" "Ava Element Classic" "Ava Element Classic oma seadmes" + "Ava „Seadistused“ → „Turvalisus ja privaatsus“" + "Krüptovõtmete halduses vali „Krüptitud sõnumite taastamine“" + "Võtmehoidla kasutuselevõtmiseks palun järgi juhendit" + "Tule tagasi rakendusse %1$s" + "Enne jätkamist rakenduses %1$s võta oma võtmehoidla kasutusele" "Versioon %1$s" + "Kontrollin kasutajakontot" "Logi sisse käsitsi" "Logi sisse serverisse %1$s" "Logi sisse QR-koodi alusel" diff --git a/features/login/impl/src/main/res/values-eu/translations.xml b/features/login/impl/src/main/res/values-eu/translations.xml index 355e63546c..78a13e5810 100644 --- a/features/login/impl/src/main/res/values-eu/translations.xml +++ b/features/login/impl/src/main/res/values-eu/translations.xml @@ -21,7 +21,7 @@ "Sortu kontua" "Kontu hau desaktibatuta dago." "Erabiltzaile-izena edo/eta pasahitza okerrak" - "Hautatutako zerbitzaria ez da bateragarria pasahitz edo OIDC saio-hasierarekin. Jarri harremanetan administratzailearekin edo aukeratu beste zerbitzari bat." + "Hautatutako zerbitzaria ez da bateragarria pasahitz edo OAuth saio-hasierarekin. Jarri harremanetan administratzailearekin edo aukeratu beste zerbitzari bat." "Sartu zure datuak" "Matrix komunikazio seguru eta deszentralizaturako sare irekia da." "Ongi etorri!" diff --git a/features/login/impl/src/main/res/values-fa/translations.xml b/features/login/impl/src/main/res/values-fa/translations.xml index ef7062a88a..c28c891c9a 100644 --- a/features/login/impl/src/main/res/values-fa/translations.xml +++ b/features/login/impl/src/main/res/values-fa/translations.xml @@ -15,15 +15,17 @@ "تغییر فراهم کنندهٔ حساب" "پلی گپگل" "ما نتوانستیم به این کارساز خانگی برسیم. لطفاً بررسی کنید که URL کارساز اصلی را به درستی وارد کرده اید. اگر URL صحیح است، برای کمک بیشتر با مدیر کارساز خانگی خود تماس بگیرید." + "سرور به دلیل مشکلی در فایل .well-known در دسترس نیست: %1$s" "نشانی کارساز خانگی" "ورود نشانی دامنه." "نشانی کارسازتان چیست؟" "کارسازتان را برگزینید" "ایجاد حساب" - "این حساب از کار افتاده است." + "این حساب حذف شده است." "نام کاربری یا گذرواژه نامعتبر است" "این یک شناسه کاربری معتبر نیست. قالب صحیح: ‪«@user:homeserver.or" - "کارساز اصلی انتخاب شده از رمز عبور یا ورود OIDC پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک کارساز خانگی دیگر را انتخاب کنید." + "این سرور برای استفاده از توکن‌های به‌روزرسانی پیکربندی شده است. این توکن‌ها هنگام استفاده از ورود مبتنی بر رمز عبور پشتیبانی نمی‌شوند." + "کارساز اصلی انتخاب شده از رمز عبور یا ورود OAuth پشتیبانی نمی کند. لطفا با مدیر خود تماس بگیرید یا یک کارساز خانگی دیگر را انتخاب کنید." "جزییاتتان را وارد کنید" "ماتریکس شبکه‌ای بار برای ارتباطات نامتمرکز و امن است." "خوش برگشتید!" diff --git a/features/login/impl/src/main/res/values-fi/translations.xml b/features/login/impl/src/main/res/values-fi/translations.xml index 0d795811b1..8561bcfce3 100644 --- a/features/login/impl/src/main/res/values-fi/translations.xml +++ b/features/login/impl/src/main/res/values-fi/translations.xml @@ -28,11 +28,11 @@ "Mikä on palvelimesi osoite?" "Valitse palvelimesi" "Luo tili" - "Tämä tili on deaktivoitu." + "Tämä tili on poistettu." "Väärä käyttäjänimi ja/tai salasana" "Tämä ei ole kelvollinen käyttäjätunnus. Odotettu muoto: \'@käyttäjä:kotipalvelin.fi\'" "Tämä palvelin on määritetty käyttämään refresh tokeneja. Näitä ei tueta salasanapohjaisen kirjautumisen kanssa." - "Valitsemasi kotipalvelin ei tue salasana- tai OIDC-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin." + "Valitsemasi kotipalvelin ei tue salasana- tai OAuth-kirjautumista. Ota yhteyttä palvelimesi ylläpitäjään tai valitse toinen kotipalvelin." "Anna tietosi" "Matrix on avoin verkko turvallista, hajautettua viestintää varten." "Tervetuloa takaisin!" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 3435eb7d40..504517aad7 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -28,11 +28,11 @@ "Quelle est l’adresse de votre serveur ?" "Choisissez votre serveur" "Créer un compte" - "Ce compte a été désactivé." + "Ce compte a été supprimé." "Nom d’utilisateur et/ou mot de passe incorrects" "Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »" "Ce serveur est configuré pour utiliser des tokens d’actualisation. Ils ne sont pas pris en charge lors de l’utilisation d’une connexion basée sur un mot de passe." - "Le serveur d’accueil sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur d’accueil." + "Le serveur d’accueil sélectionné ne prend pas en charge le mot de passe ou la connexion OAuth. Contactez votre administrateur ou choisissez un autre serveur d’accueil." "Saisissez vos identifiants" "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." "Content de vous revoir !" diff --git a/features/login/impl/src/main/res/values-hr/translations.xml b/features/login/impl/src/main/res/values-hr/translations.xml index b52dd77d7f..f578790380 100644 --- a/features/login/impl/src/main/res/values-hr/translations.xml +++ b/features/login/impl/src/main/res/values-hr/translations.xml @@ -32,7 +32,7 @@ "Netočno korisničko ime i/ili zaporka" "To nije valjani identifikator korisnika. Očekivani oblik: ‘@korisnik:matičniposlužitelj.org’" "Ovaj je poslužitelj konfiguriran za korištenje tokena za osvježavanje. Oni nisu podržani kada se upotrebljava prijava temeljena na zaporki." - "Odabrani matični poslužitelj ne podržava zaporku ili OIDC prijavu. Obratite se administratoru ili odaberite drugi matični poslužitelj." + "Odabrani matični poslužitelj ne podržava zaporku ili OAuth prijavu. Obratite se administratoru ili odaberite drugi matični poslužitelj." "Unesite svoje podatke" "Matrix je otvorena mreža za sigurnu, decentraliziranu komunikaciju." "Dobro došli natrag!" diff --git a/features/login/impl/src/main/res/values-hu/translations.xml b/features/login/impl/src/main/res/values-hu/translations.xml index c2b2fefddb..26534cc523 100644 --- a/features/login/impl/src/main/res/values-hu/translations.xml +++ b/features/login/impl/src/main/res/values-hu/translations.xml @@ -28,11 +28,11 @@ "Mi a kiszolgálója címe?" "Válassza ki a kiszolgálóját" "Fiók létrehozása" - "Ez a fiók deaktiválva lett." + "Ez a fiók törölve lett." "Helytelen felhasználónév vagy jelszó" "Ez nem érvényes felhasználóazonosító. A várt formátum: „@user:homeserver.org”" "Ez a kiszolgáló frissítési tokenek használatára van beállítva. Ezek jelszó alapú bejelentkezés esetén nem támogatottak." - "A kiválasztott Matrix-kiszolgáló nem támogatja a jelszavas vagy OIDC-alapú bejelentkezést. Lépjen kapcsolatba a kiszolgáló adminisztrátorával, vagy válasszon másik Matrix-kiszolgálót." + "A kiválasztott Matrix-kiszolgáló nem támogatja a jelszavas vagy OAuth-alapú bejelentkezést. Lépjen kapcsolatba a kiszolgáló adminisztrátorával, vagy válasszon másik Matrix-kiszolgálót." "Adja meg adatait" "A Matrix egy nyitott hálózat a biztonságos, decentralizált kommunikációhoz." "Örülünk, hogy visszatért!" diff --git a/features/login/impl/src/main/res/values-in/translations.xml b/features/login/impl/src/main/res/values-in/translations.xml index e05fd8746d..4b9f3ffdef 100644 --- a/features/login/impl/src/main/res/values-in/translations.xml +++ b/features/login/impl/src/main/res/values-in/translations.xml @@ -32,7 +32,7 @@ "Nama pengguna dan/atau kata sandi salah" "Ini bukan pengenal pengguna yang valid. Format yang diharapkan: \'@pengguna:homeserver.org\'" "Server ini diatur untuk menggunakan token penyegaran. Ini tidak didukung ketika menggunakan log masuk berbasis kata sandi." - "Homeserver yang dipilih tidak mendukung log masuk kata sandi atau OIDC. Silakan hubungi admin Anda atau pilih homeserver yang lain." + "Homeserver yang dipilih tidak mendukung log masuk kata sandi atau OAuth. Silakan hubungi admin Anda atau pilih homeserver yang lain." "Masukkan detail Anda" "Matrix adalah jaringan terbuka untuk komunikasi yang aman dan terdesentralisasi." "Selamat datang kembali!" 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 e882654e4b..1f1b51cda8 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -28,11 +28,11 @@ "Qual è l\'indirizzo del tuo server?" "Seleziona il tuo server" "Crea account" - "Questo account è stato disattivato." + "Questo account è stato eliminato." "Nome utente e/o password errati" "Questo non è un identità utente valida. il formato atteso é: \'@user:homeserver.org\'" "Questo server è configurato per usare i token di aggiornamento. Non sono supportati quando si usa l\'accesso basato su password." - "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." + "L\'homeserver selezionato non supporta la password o l\'accesso OAuth. Contatta il tuo amministratore o scegli un altro homeserver." "Inserisci i tuoi dati" "Matrix è una rete aperta per comunicazioni sicure e decentralizzate." "Bentornato!" @@ -45,6 +45,7 @@ "Torna a %1$s" "Abilita l\'archivio delle chiavi prima di procedere con %1$s" "Versione %1$s" + "Verifica dell\'account" "Accedi manualmente" "Accedi a %1$s" "Accedi con 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 index 219605ab22..42698e27dc 100644 --- a/features/login/impl/src/main/res/values-ja/translations.xml +++ b/features/login/impl/src/main/res/values-ja/translations.xml @@ -1,18 +1,18 @@ - "アカウントの提供元を変更" + "アカウント提供元を変更" "ホームサーバーのアドレス" - "検索用のキーワードまたはドメインのアドレスを入力してください。" + "検索のキーワードまたはドメインのアドレスを入力してください。" "会社やコミュニティ, 個人のサーバーなどを検索します。" - "アカウントの提供元を検索" - "メールアプリのように、あなたの会話はここに保管されています。" + "アカウント提供元を検索" + "メールアプリのように、あなたの会話はこのサーバー上に保管されます。" "%s にサインインを試みています" - "メールアプリのように、あなたの会話はここに保管されています。" - "%s にアカウントの作成を試みています" + "メールアプリのように、あなたの会話はこのサーバー上に保管されます。" + "%s 上にアカウントを作成しようとしています" "Matrix.org は Matrix.org Foundation が運営する、大規模で安全な分散型コミュニケーションを実現する無償のサーバーです。" "その他" - "自身のサーバーや仕事用のアカウントにサインインするには、アカウント提供元のサーバーを指定してください。" - "アカウントの提供元を変更" + "自身のサーバーや仕事用のアカウントにサインインするには、アカウント提供元を変更してください。" + "アカウント提供元を変更" "Google Play" "%1$s では Element Pro を使用する必要があります。アプリストアよりダウンロードしてください。" "Element Pro が必要です" @@ -27,11 +27,11 @@ "サーバーのアドレスは何ですか?" "サーバーを選択" "アカウントを作成" - "このアカウントは無効化されています。" + "アカウントは削除されました。" "ユーザー名またはパスワードが違います" "無効なユーザーIDです。正しい形式は \"@ユーザー:ホームサーバー\" です。" "このサーバーはリフレッシュトークンを使用します。パスワードを使用したログインとは併用できません。" - "指定したホームサーバはパスワードまたはOIDCによるログインに対応していません。管理者に問い合わせるか、異なるホームサーバーを使用してください。" + "指定したホームサーバはパスワードまたはOAuthによるログインに対応していません。管理者に問い合わせるか、異なるホームサーバーを使用してください。" "詳細を入力" "Matrix は安全で分散型のオープンなネットワークです。" "お待ちしておりました。" @@ -80,8 +80,8 @@ "%1$s に非対応" "読み取る" "コンピュータで %1$s を開く" - "アバターをタップしてください" - "%1$s を選択してください" + "アバターをタップ" + "%1$s を選択" "\"新しい端末を追加\"" "この端末でQRコードを読み取る" "アカウント提供元が対応する場合にのみ使用できます。" @@ -98,11 +98,11 @@ "一方の端末を待機しています" "アカウント提供元が、サインインを検証するために以下の文字列を要求することがあります。" "検証コード" - "アカウントの提供元を変更" + "アカウント提供元を変更" "Element 開発者用の非公開のサーバーです。" "Matrix は安全で分散型のオープンなネットワークです。" - "メールアプリのように、あなたの会話はここに保管されています。" + "メールアプリのように、あなたの会話はこのサーバー上に保管されます。" "%1$s にサインインを試みています" "アカウント提供元を選択" - "%1$s 上にアカウントの作成を試みています" + "%1$s 上にアカウントを作成しようとしています" diff --git a/features/login/impl/src/main/res/values-ka/translations.xml b/features/login/impl/src/main/res/values-ka/translations.xml index 54e277ec12..04db3fda13 100644 --- a/features/login/impl/src/main/res/values-ka/translations.xml +++ b/features/login/impl/src/main/res/values-ka/translations.xml @@ -23,7 +23,7 @@ "არასწორი მომხმარებლის სახელი და/ან პაროლი" "მოცემული მომხმარებლის იდენტიფიკატორი არასწორია. დასაშვები ფორმატი: ‘@user:homeserver.org’" "ეს სერვერი კონფიგურირებულია განახლების გასაღებების გამოსაყენებლად. პაროლზე დაფუძნებული შეცვლისას ისინი მხარდაჭერილი არაა." - "მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OIDC-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი." + "მოცემული სახლის სერვერი მხარს არ უჭერს პაროლით ან OAuth-ით შესვლას. გთხოვთ, დაუკავშირდეთ თქვენს ადმინისტრატორს ან აარჩიეთ სხვა სახლის სერვერი." "შეიყვანეთ თქვენი დეტალები" "Matrix არის ღია ქსელი უსაფრთხო, დეცენტრალიზებული კომუნიკაციისთვის." "კეთილი იყოს თქვენი მობრძანება!" diff --git a/features/login/impl/src/main/res/values-ko/translations.xml b/features/login/impl/src/main/res/values-ko/translations.xml index c870377fc4..8f313b15ee 100644 --- a/features/login/impl/src/main/res/values-ko/translations.xml +++ b/features/login/impl/src/main/res/values-ko/translations.xml @@ -28,11 +28,11 @@ "서버의 주소는 무엇인가요?" "서버 선택" "계정 만들기" - "계정이 비활성화되었습니다." + "삭제된 계정입니다." "잘못된 아이디/비밀번호" "이 사용자 ID는 유효하지 않습니다. 예상 형식: ‘@user:homeserver.org’" "이 서버는 새로 고침 토큰을 사용하도록 구성되어 있습니다. 비밀번호 기반 로그인을 사용하는 경우 이 기능은 지원되지 않습니다." - "선택한 홈 서버는 password 또는 OIDC 로그인을 지원하지 않습니다. 관리자에게 문의하거나 다른 홈 서버를 선택하세요." + "선택한 홈 서버는 password 또는 OAuth 로그인을 지원하지 않습니다. 관리자에게 문의하거나 다른 홈 서버를 선택하세요." "귀하의 세부 정보를 입력하십시오" "Matrix 는 안전하고 분산된 커뮤니케이션을 위한 개방형 네트워크입니다." "다시 돌아온 걸 환영합니다!" @@ -45,6 +45,7 @@ "%1$s(으)로 돌아가기" "%1$s(으)로 진행하기 전에 키 저장소를 활성화해 주세요." "버전 %1$s" + "계정 확인 중" "수동으로 로그인" "%1$s 에 로그인합니다" "QR 코드로 로그인" 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 870e2c4d31..dc1a2b7ba6 100644 --- a/features/login/impl/src/main/res/values-lt/translations.xml +++ b/features/login/impl/src/main/res/values-lt/translations.xml @@ -31,7 +31,7 @@ "Ši paskyra buvo išjungta." "Neteisingas vartotojo vardas ir (arba) slaptažodis" "Tai nėra tinkamas vartotojo vardas. Reikalingas formatas: \'@vartotojas:serveris.org\'" - "Pasirinktas serveris nepalaiko slaptažodžio ar OIDC prisijungimo. Susisiekite su serverio administracija arba pasirinkite kitą serverį." + "Pasirinktas serveris nepalaiko slaptažodžio ar OAuth prisijungimo. Susisiekite su serverio administracija arba pasirinkite kitą serverį." "Įveskite savo duomenis" "Matrix yra atviras tinklas, skirtas saugiam, decentralizuotam bendravimui." "Sveiki sugrįžę!" diff --git a/features/login/impl/src/main/res/values-nb/translations.xml b/features/login/impl/src/main/res/values-nb/translations.xml index 5957b902ce..6645691b84 100644 --- a/features/login/impl/src/main/res/values-nb/translations.xml +++ b/features/login/impl/src/main/res/values-nb/translations.xml @@ -32,7 +32,7 @@ "Feil brukernavn og/eller passord" "Dette er ikke en gyldig brukeridentifikator. Forventet format: \'@bruker:homeserver.org\'" "Denne serveren er konfigurert til å bruke oppdateringstokener. Disse støttes ikke når du bruker passordbasert pålogging." - "Den valgte hjemmeserveren støtter ikke passord eller OIDC-pålogging. Ta kontakt med administratoren din eller velg en annen hjemmeserver." + "Den valgte hjemmeserveren støtter ikke passord eller OAuth-pålogging. Ta kontakt med administratoren din eller velg en annen hjemmeserver." "Skriv inn opplysningene dine" "Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon." "Velkommen tilbake!" diff --git a/features/login/impl/src/main/res/values-nl/translations.xml b/features/login/impl/src/main/res/values-nl/translations.xml index 8a7f695156..e5d90e2234 100644 --- a/features/login/impl/src/main/res/values-nl/translations.xml +++ b/features/login/impl/src/main/res/values-nl/translations.xml @@ -24,7 +24,7 @@ "Onjuiste gebruikersnaam en/of wachtwoord" "Dit is geen geldige gebruikers-ID. Verwacht formaat: \'@user:homeserver.org\'" "Deze server is geconfigureerd om verversingstokens te gebruiken. Deze worden niet ondersteund bij inloggen met een wachtwoord." - "De geselecteerde homeserver ondersteunt geen wachtwoord of OIDC aanmelding. Neem contact op met je beheerder of kies een andere homeserver." + "De geselecteerde homeserver ondersteunt geen wachtwoord of OAuth aanmelding. Neem contact op met je beheerder of kies een andere homeserver." "Vul je gegevens in" "Matrix is een open netwerk voor veilige, gedecentraliseerde communicatie." "Welkom terug!" diff --git a/features/login/impl/src/main/res/values-pl/translations.xml b/features/login/impl/src/main/res/values-pl/translations.xml index 0c7810abe7..634d856cdf 100644 --- a/features/login/impl/src/main/res/values-pl/translations.xml +++ b/features/login/impl/src/main/res/values-pl/translations.xml @@ -28,20 +28,29 @@ "Jaki jest adres Twojego serwera?" "Wybierz swój serwer" "Utwórz konto" - "To konto zostało dezaktywowane." + "To konto zostało usunięte." "Nieprawidłowa nazwa użytkownika i/lub hasło" "To nie jest prawidłowy identyfikator użytkownika. Oczekiwany format: \'@user:homeserver.org\'" "Ten serwer został skonfigurowany do korzystania z tokenów odświeżania. Nie są one obsługiwane, gdy korzystasz z hasła." - "Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OIDC. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy." + "Wybrany serwer domowy nie obsługuje uwierzytelniania hasłem, ani OAuth. Skontaktuj się z jego administratorem lub wybierz inny serwer domowy." "Wprowadź swoje dane" "Matrix to otwarta sieć do bezpiecznej i zdecentralizowanej komunikacji." "Witaj ponownie!" "Zaloguj się do %1$s" + "Otwórz Element Classic" + "Otwórz Element Classic na swoim urządzeniu" + "Przejdź do Ustawienia > Bezpieczeństwo i prywatność" + "W Zarządzaniu kluczami kryptograficznymi wybierz przywracanie wiadomości szyfrowanych" + "Aby włączyć magazyn kluczy, postępuj zgodnie z instrukcjami" + "Wróć do %1$s" + "Włącz magazyn kluczy zanim przejdziesz do %1$s" "Wersja %1$s" + "Sprawdzanie konta" "Zaloguj się ręcznie" "Zaloguj się do %1$s" "Zaloguj się za pomocą kodu QR" "Utwórz konto" + "Witamy ponownie" "Witamy w %1$s. Szybszy i prostszy niż kiedykolwiek." "Witamy w %1$s. Doładowany, dla szybkości i prostoty." "Be in your element" @@ -60,6 +69,8 @@ "Prośba o logowanie została anulowana" "Logowanie zostało odrzucone na drugim urządzeniu." "Logowanie odrzucone" + "Nie musisz już robić nic więcej." + "Twoje drugie urządzenie jest już zalogowane" "Logowanie wygasło. Spróbuj ponownie." "Logowanie nie zostało ukończone na czas" "Twoje drugie urządzenie nie wspiera logowania się do %s za pomocą kodu QR. @@ -86,7 +97,7 @@ Spróbuj zalogować się ręcznie lub zeskanuj kod QR na innym urządzeniu.""Zacznij od nowa" "Wystąpił nieoczekiwany błąd. Spróbuj ponownie." "Oczekiwanie na drugie urządzenie" - "Twój dostawca konta może poprosić o podany kod, aby zweryfikować logowanie." + "Twój dostawca konta może poprosić o podany kod w celu weryfikacji logowania." "Twój kod weryfikacyjny" "Zmień dostawcę konta" "Serwer prywatny dla pracowników Element." diff --git a/features/login/impl/src/main/res/values-pt-rBR/translations.xml b/features/login/impl/src/main/res/values-pt-rBR/translations.xml index afca14d201..ad2ad2b5fb 100644 --- a/features/login/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/login/impl/src/main/res/values-pt-rBR/translations.xml @@ -32,7 +32,7 @@ "Nome de usuário e/ou senha incorretos" "Esse não é um identificador de usuário válido. Formato esperado: \'@usuário:servidor.org\'" "Este servidor está configurado para usar tokens recarregados. Não há suporte a eles ao entrar por uma senha." - "O servidor selecionado não suporta a entrada por senha ou OIDC. Entre em contato com o administrador ou escolha outro servidor." + "O servidor selecionado não suporta a entrada por senha ou OAuth. Entre em contato com o administrador ou escolha outro servidor." "Digite seus dados" "A Matrix é uma rede aberta para comunicação segura e descentralizada." "Boas-vindas novamente!" diff --git a/features/login/impl/src/main/res/values-pt/translations.xml b/features/login/impl/src/main/res/values-pt/translations.xml index c2aa0d5ed6..ff3cae843d 100644 --- a/features/login/impl/src/main/res/values-pt/translations.xml +++ b/features/login/impl/src/main/res/values-pt/translations.xml @@ -28,11 +28,11 @@ "Qual é o endereço do teu servidor?" "Seleciona o teu servidor" "Criar conta" - "Esta conta foi desativada." + "Esta conta foi eliminada." "Nome de utilizador ou senha incorretos" "Identificador de utilizador inválido. Formato esperado: ‘@utilizador:servidor.org’" "Este servidor está configurado para utilizar \"tokens\" de atualização. Estes não são suportados quando utilizas o início de sessão por senha." - "O servidor selecionado não suporta início de sessão por senha nem por OIDC. Por favor, contacta o teu administrador ou escolhe outro servidor." + "O servidor selecionado não suporta início de sessão por senha nem por OAuth. Por favor, contacta o teu administrador ou escolhe outro servidor." "Insere o teus detalhes" "A Matrix é uma rede aberta de comunicação descentralizada e segura." "Bem-vindo(a) de volta!" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 6dddc4d0bf..b44242abf8 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -28,20 +28,29 @@ "Care este adresa serverului dumneavoastră?" "Selectați serverul dumneavoastra" "Creați un cont" - "Acest cont a fost dezactivat." + "Acest cont a fost șters." "Utilizator și/sau parolă incorecte" "Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”" "Acest server este configurat pentru a utiliza token-uri de reîmprospătare. Acestea nu sunt acceptate atunci când utilizați autentificare bazată pe parolă." - "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." + "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OAuth. Te rugăm să contactezi administratorul sau să alegi un alt homeserver." "Introduceți detaliile" "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." "Bine ați revenit!" "Conectați-vă la %1$s" + "Deschideți Element Clasic" + "Deschideți Element Classic pe dispozitivul dumneavoastră" + "Accesați Setări > Securitate și confidențialitate" + "În Gestionarea cheilor criptografice, selectați Recuperarea mesajelor criptate" + "Urmați instrucțiunile pentru a activa stocarea cheilor" + "Reveniți la %1$s" + "Activați stocarea cheilor înainte de a continua către %1$s" "Versiunea %1$s" + "Se verifică contul…" "Conectați-vă manual" "Conectați-vă la %1$s" "Conectați-vă cu un cod QR" "Creați un cont" + "Bine ați revenit" "Bine ați venit la cel mai rapid %1$s din toate timpurile. Supraalimentat pentru viteză și simplitate." "Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate." "Fii în Elementul tău" @@ -60,6 +69,8 @@ "Cererea de autentificare a fost anulată" "Autentificarea a fost refuzată pe celălalt dispozitiv." "Autentificarea a fost refuzată" + "Nu trebuie să faceți nimic altceva." + "Celălalt dispozitiv este deja conectat" "Autentificarea a expirat. Vă rugăm să încercați din nou." "Autentificarea nu a fost finalizată la timp" "Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR. diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index 4437c19056..5afbe1f513 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -28,14 +28,14 @@ "Какой адрес у вашего сервера?" "Выберите свой сервер" "Создать аккаунт" - "Данная учётная запись была отключена." + "Эта учетная запись была удалена." "Неверное имя пользователя и/или пароль" "Это некорректный идентификатор пользователя. Правильный формат: @user:homeserver.org" "Этот сервер настроен на использование токенов обновления. Они не поддерживаются при использовании входа на основе пароля." - "Выбранный сервер не поддерживает вход по паролю и OIDC. Пожалуйста, свяжитесь с администратором или выберите другой сервер." + "Выбранный сервер не поддерживает вход по паролю и OAuth. Пожалуйста, свяжитесь с администратором или выберите другой сервер." "Введите свои данные" "Matrix — это открытая сеть для безопасной децентрализованной связи." - "Рады видеть вас снова!" + "Рады видеть Вас снова!" "Войти в %1$s" "Открыть Element Classic" "Откройте Element Classic на своем устройстве." @@ -69,9 +69,9 @@ "Запрос на вход отменен" "Вход в систему был отклонен на другом устройстве." "Вход отклонен" - "На этом все." + "На этом всё." "Вход уже выполнен на другом устройстве" - "Срок действия входа истек. Пожалуйста, попробуйте еще раз." + "Срок действия входа истек. Пожалуйста, попробуйте ещё раз." "Вход в систему не был выполнен вовремя" "Другое устройство не поддерживает вход в %s с помощью QR-кода. @@ -95,7 +95,7 @@ "Разрешите доступ к камере для сканирования QR-кода" "Сканировать QR-код" "Начать заново" - "Произошла непредвиденная ошибка. Пожалуйста, попробуйте еще раз." + "Произошла непредвиденная ошибка. Пожалуйста, попробуйте ещё раз." "Ожидание другого устройства" "Сервер для аккаунта может запросить следующий код для подтверждения входа." "Ваш код подтверждения" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index 9bd494dd56..b2c3077de2 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -32,7 +32,7 @@ "Nesprávne používateľské meno a/alebo heslo" "Toto nie je platný identifikátor používateľa. Očakávaný formát: \'@pouzivatel:homeserver.sk\'" "Tento server je nakonfigurovaný tak, aby používal obnovovacie tokeny. Pri prihlasovaní na základe hesla nie sú podporované." - "Vybraný domovský server nepodporuje prihlásenie pomocou hesla alebo OIDC. Obráťte sa na správcu alebo vyberte iný domovský server." + "Vybraný domovský server nepodporuje prihlásenie pomocou hesla alebo OAuth. Obráťte sa na správcu alebo vyberte iný domovský server." "Zadajte svoje údaje" "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." "Vitajte späť!" diff --git a/features/login/impl/src/main/res/values-sv/translations.xml b/features/login/impl/src/main/res/values-sv/translations.xml index 33fb76b5bd..396f2025b7 100644 --- a/features/login/impl/src/main/res/values-sv/translations.xml +++ b/features/login/impl/src/main/res/values-sv/translations.xml @@ -32,7 +32,7 @@ "Felaktigt användarnamn och/eller lösenord" "Detta är inte en giltig användaridentifierare. Förväntat format: \'@användare:hemserver.org\'" "Den här servern är konfigurerad för att använda uppdateringstokens. Dessa stöds inte när du använder lösenordsbaserad inloggning." - "Den valda hemservern stöder inte lösenord eller OIDC-inloggning. Kontakta administratören eller välj en annan hemserver." + "Den valda hemservern stöder inte lösenord eller OAuth-inloggning. Kontakta administratören eller välj en annan hemserver." "Ange dina uppgifter" "Matrix är ett öppet nätverk för säker, decentraliserad kommunikation." "Välkommen tillbaka!" diff --git a/features/login/impl/src/main/res/values-tr/translations.xml b/features/login/impl/src/main/res/values-tr/translations.xml index 1574fca3a8..05bb9bab15 100644 --- a/features/login/impl/src/main/res/values-tr/translations.xml +++ b/features/login/impl/src/main/res/values-tr/translations.xml @@ -28,7 +28,7 @@ "Yanlış kullanıcı adı ve/veya şifre" "Bu geçerli bir kullanıcı tanımlayıcısı değil. Kullanılması gereken biçim: \'@user:homeserver.org\'" "Bu sunucu, yenileme belirteçlerini kullanacak şekilde yapılandırılmıştır. Parola tabanlı oturum açma kullanılırken bunlar desteklenmez." - "Seçilen ana sunucu parola veya OIDC oturum açmayı desteklemiyor. Lütfen yöneticinizle iletişime geçin veya başka bir ana sunucu seçin." + "Seçilen ana sunucu parola veya OAuth oturum açmayı desteklemiyor. Lütfen yöneticinizle iletişime geçin veya başka bir ana sunucu seçin." "Bilgilerinizi girin" "Matrix, güvenli, merkezi olmayan iletişim için açık bir ağdır." "Tekrar hoş geldiniz!" diff --git a/features/login/impl/src/main/res/values-uk/translations.xml b/features/login/impl/src/main/res/values-uk/translations.xml index b93a66fed2..17632cc4fc 100644 --- a/features/login/impl/src/main/res/values-uk/translations.xml +++ b/features/login/impl/src/main/res/values-uk/translations.xml @@ -28,20 +28,29 @@ "Яка адреса вашого сервера?" "Виберіть свій сервер" "Створити обліковий запис" - "Цей обліковий запис було деактивовано." + "Цей обліковий запис було видалено." "Неправильне ім\'я користувача та/або пароль" "Це недійсний ідентифікатор користувача. Очікуваний формат: \'@user:homeserver.org\'" "Цей сервер налаштований на використання оновлюваних токенів. Вони не підтримуються, якщо використовується вхід за допомогою основі пароля." - "Обраний домашній сервер не підтримує вхід за допомогою пароля або OIDC. Зверніться до адміністратора або виберіть інший домашній сервер." + "Обраний домашній сервер не підтримує вхід за допомогою пароля або OAuth. Зверніться до адміністратора або виберіть інший домашній сервер." "Введіть свої дані" "Matrix — це відкрита мережа для безпечної, децентралізованої комунікації." "З поверненням!" "Увійти в %1$s" + "Відкрити Element Classic" + "Відкрийте Element Classic на своєму пристрої" + "Перейдіть до «Налаштування» > «Безпека та конфіденційність»" + "У розділі «Управління криптографічними ключами» виберіть «Відновлення зашифрованих повідомлень»" + "Дотримуйтесь інструкцій, щоб увімкнути сховище ключів" + "Повернутися до %1$s" + "Увімкніть сховище ключів, перш ніж переходити до %1$s" "Версія %1$s" + "Перевірка облікового запису" "Увійти вручну" "Увійти в %1$s" "Увійти за допомогою QR-коду" "Створити обліковий запис" + "З поверненням!" "Ласкаво просимо до найшвидшого %1$s. Заряджений для швидкості та простоти." "Ласкаво просимо до %1$s. Заряджений, для швидкості та простоти." "Будьте у своєму element" diff --git a/features/login/impl/src/main/res/values-ur/translations.xml b/features/login/impl/src/main/res/values-ur/translations.xml index 334d5b6ea6..6762ab16af 100644 --- a/features/login/impl/src/main/res/values-ur/translations.xml +++ b/features/login/impl/src/main/res/values-ur/translations.xml @@ -24,7 +24,7 @@ "غلط صارف نام اور/یا لفظ عبور" "یہ صالح صارف شناسه نہیں ہے۔ متوقع شکل: @صارف:منزلی خادم" "یہ خادم تازگی کی رموزِ ممیز استعمال کرنے کے لئے تشکیل دیا گیا ہے۔ لفظ عبور پر مبنی دخول استعمال کرتے ہوئے ان کی حمایت نہیں کی جاتی۔" - "منتخب منزلی خادم کلمۂ عبوری یا OIDC دخول کا تعاون نہیں کرتا۔ برائے مہربانی اپنے منتظم سے رابطہ کریں یا کوئی اور منزلی خادم چنیں۔" + "منتخب منزلی خادم کلمۂ عبوری یا OAuth دخول کا تعاون نہیں کرتا۔ برائے مہربانی اپنے منتظم سے رابطہ کریں یا کوئی اور منزلی خادم چنیں۔" "اپنی تفصیلات درج کریں" "میٹرکس محفوظ، غیر مرکزی مواصلت کے لئے ایک کھلا شبکہ ہے۔" "واپس خوش آمدید!" diff --git a/features/login/impl/src/main/res/values-uz/translations.xml b/features/login/impl/src/main/res/values-uz/translations.xml index 07b3d8835b..546397935b 100644 --- a/features/login/impl/src/main/res/values-uz/translations.xml +++ b/features/login/impl/src/main/res/values-uz/translations.xml @@ -31,7 +31,7 @@ "Notog\'ri foydalanuvchi nomi va/yoki parol" "Bu haqiqiy foydalanuvchi identifikatori emas. Kutilayotgan format: \'@user:homeserver.org\'" "Ushbu server yangilash tokenlaridan foydalanishga moslashtirilgan. Parolga asoslangan tizimga kirishda bunday tokenlar qoʻllab-quvvatlanmaydi." - "Tanlangan uy serveri parol yoki OIDC loginni qo\'lab-quvvatlamaydi. Iltimos, administratoringizga murojaat qiling yoki boshqa uy serverini tanlang." + "Tanlangan uy serveri parol yoki OAuth loginni qo\'lab-quvvatlamaydi. Iltimos, administratoringizga murojaat qiling yoki boshqa uy serverini tanlang." "Tafsilotlaringizni kiriting" "Matrix xavfsiz, markazlashmagan aloqa uchun ochiq tarmoqdir." "Qaytib kelganingizdan xursandmiz!" diff --git a/features/login/impl/src/main/res/values-vi/translations.xml b/features/login/impl/src/main/res/values-vi/translations.xml index 4d9b921ea5..66089a4708 100644 --- a/features/login/impl/src/main/res/values-vi/translations.xml +++ b/features/login/impl/src/main/res/values-vi/translations.xml @@ -31,7 +31,7 @@ "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." + "Homeserver đã chọn không hỗ trợ đăng nhập bằng mật khẩu hoặc OAuth. 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!" diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index 7170288c5d..c4d16f5928 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -32,7 +32,7 @@ "不正確的使用者名稱或密碼" "此非有效的使用者識別字串。預期的格式:‘@user:homeserver.org’" "此伺服器已設定為使用重新整理權杖。使用以密碼為基礎的登入方式時,不支援這些功能。" - "選定的家伺服器不支援密碼或 OIDC 登入。請聯絡您的管理員或選擇其他家伺服器。" + "選定的家伺服器不支援密碼或 OAuth 登入。請聯絡您的管理員或選擇其他家伺服器。" "輸入您的詳細資料" "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "歡迎回來!" diff --git a/features/login/impl/src/main/res/values-zh/translations.xml b/features/login/impl/src/main/res/values-zh/translations.xml index ac125ba945..16afe7d5bd 100644 --- a/features/login/impl/src/main/res/values-zh/translations.xml +++ b/features/login/impl/src/main/res/values-zh/translations.xml @@ -1,10 +1,10 @@ - "更改账户提供方" + "更改账户提供者" "主服务器地址" "输入搜索词或域名地址。" "搜索公司、社区或私人服务器。" - "寻找账户提供方" + "查找账户提供者" "这是你的对话将存在的地方,就像你使用邮件提供者来存储电子邮件那样。" "你即将登录到 %s" "这是你的对话将存在的地方,就像你使用邮件提供者来存储电子邮件那样。" @@ -12,7 +12,7 @@ "Matrix.org 由 Matrix.org 基金会运营,是用于安全、去中心化的通信的公共 Matrix 网络上的大型免费服务器。" "其它" "使用其它账户提供者,例如你自己的私有服务器或工作账户。" - "更改账户提供方" + "更改账户提供者" "Google Play" "%1$s 要求 Element Pro。请从应用商店下载。" "需要 Element Pro" @@ -21,18 +21,18 @@ %1$s" "所选账户提供者不支持滑动同步。需要升级服务器才能使用 %1$s。" "%1$s 不允许连接到 %2$s。" - "本应用已配置为允许访问:%1$s 。" + "此 app 已配置为允许访问:%1$s。" "账户提供者 %1$s 不被允许。" "主服务器 URL" "输入域名地址。" "你的服务器地址是什么?" "选择服务器" "创建账户" - "该账户已被停用。" + "此账户已被删除。" "用户名与(或)密码不正确" "这不是合法的用户 ID。预期格式:“@user:homeserver.org”。" "此服务器使用刷新令牌。使用密码登录时不支持这些功能。" - "该服务器不支持密码登录与 OIDC 登录。请联系服务器管理员或选择另一服务器。" + "该服务器不支持密码登录与 OAuth 登录。请联系服务器管理员或选择另一服务器。" "输入详细信息" "Matrix 是一个用于安全、去中心化通信的开放网络。" "欢迎回来!" @@ -51,7 +51,7 @@ "使用二维码登录" "创建账户" "欢迎回来" - "欢迎使用 %1$s,快而简约的消息应用。" + "欢迎使用迄今最快的 %1$s,速度与简洁的极致。" "欢迎使用 %1$s,速度与简洁的极致。" "融入 Element" "建立安全连接" @@ -77,7 +77,7 @@ 尝试手动或使用另一个设备扫描二维码." "二维码不受支持" - "账户提供方不支持 %1$s." + "账户提供者不支持 %1$s." "不支持 %1$s." "准备进行扫描" "在桌面设备上打开 %1$s" @@ -99,7 +99,7 @@ "正在等待其它设备" "你的账户提供者可能会要求你提供以下代码以验证登录。" "你的验证码" - "更改账户提供方" + "更改账户提供者" "专为 Element 员工提供的私人服务器。" "Matrix 是一个用于安全、去中心化通信的开放网络。" "这是你的对话将存在的地方,就像你使用邮件提供者来存储电子邮件那样。" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index f8d5ffa651..10fb6ef04a 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -32,7 +32,7 @@ "Incorrect username and/or password" "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’" "This server is configured to use refresh tokens. These aren\'t supported when using password based login." - "The selected homeserver doesn\'t support password or OIDC login. Please contact your admin or choose another homeserver." + "The selected homeserver doesn\'t support password or OAuth login. Please contact your admin or choose another homeserver." "Enter your details" "Matrix is an open network for secure, decentralised communication." "Welcome back!" 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 86a629270f..a05194d008 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 @@ -17,7 +17,7 @@ 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.libraries.oauth.test.customtab.FakeOAuthActionFlow import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import kotlinx.coroutines.test.runTest @@ -39,7 +39,7 @@ class DefaultLoginEntryPointTest { buildContext = buildContext, plugins = plugins, accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), - oidcActionFlow = FakeOidcActionFlow(), + oAuthActionFlow = FakeOAuthActionFlow(), appCoroutineScope = backgroundScope, elementClassicConnection = FakeElementClassicConnection(), preferencesEntryPoint = FakePreferencesEntryPoint(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt index 21b3cca600..feeeb982c4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginIntentResolverTest.kt @@ -8,14 +8,12 @@ package io.element.android.features.login.impl -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.features.login.api.LoginParams +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultLoginIntentResolverTest { +class DefaultLoginIntentResolverTest : RobolectricTest() { @Test fun `nominal case`() { val sut = DefaultLoginIntentResolver() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 1fb5d37627..274b58ee49 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -50,7 +50,7 @@ class ChangeServerPresenterTest { fun `present - change server ok`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) createPresenter( 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 index 5da3c97f3c..702adaaa6d 100644 --- 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 @@ -15,9 +15,6 @@ 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 @@ -31,17 +28,15 @@ import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatib 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 io.element.android.tests.testutils.robolectric.RobolectricTest 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 { +class DefaultElementClassicConnectionTest : RobolectricTest() { @Test fun `connection can be started Element Classic service can be bound`() = runTest { val connection = createDefaultElementClassicConnection( @@ -112,21 +107,6 @@ class DefaultElementClassicConnectionTest { } } - @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( @@ -514,17 +494,10 @@ class DefaultElementClassicConnectionTest { 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/qrcode/QrCodeLoginFlowNodeTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt index 9d2628005c..c56b4d5208 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/qrcode/QrCodeLoginFlowNodeTest.kt @@ -8,7 +8,6 @@ package io.element.android.features.login.impl.qrcode -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumble.appyx.core.modality.AncestryInfo import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl @@ -22,16 +21,15 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class QrCodeLoginFlowNodeTest { +class QrCodeLoginFlowNodeTest : RobolectricTest() { @Test fun `backstack changes when confirmation steps are received`() = runTest { val qrCodeLoginManager = FakeQrCodeLoginManager() @@ -79,7 +77,7 @@ class QrCodeLoginFlowNodeTest { qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.ConnectionInsecure) assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.InsecureChannelDetected)) - qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OidcMetadataInvalid) + qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.OAuthMetadataInvalid) assertThat(flowNode.currentNavTarget()).isEqualTo(QrCodeLoginFlowNode.NavTarget.Error(QrCodeErrorScreenType.UnknownError)) qrCodeLoginManager.currentLoginStep.value = QrCodeLoginStep.Failed(QrLoginException.Unknown) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt index f7ff5d384d..b036e07e92 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -6,17 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.chooseaccountprovider import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -25,36 +27,30 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class ChooseAccountProviderViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class ChooseAccountProviderViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setChooseAccountProviderView( + setChooseAccountProviderView( state = aChooseAccountProviderState( eventSink = eventSink, ), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `selecting an account provider emits the the expected event`() { + fun `selecting an account provider emits the the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setChooseAccountProviderView( + setChooseAccountProviderView( state = aChooseAccountProviderState( accountProviders = listOf( ChooseAccountProviderPresenterTest.accountProvider1, @@ -64,27 +60,27 @@ class ChooseAccountProviderViewTest { eventSink = eventSink, ), ) - rule.onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() + onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) } @Test - fun `when error is displayed - closing the dialog emits the expected event`() { + fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setChooseAccountProviderView( + setChooseAccountProviderView( state = aChooseAccountProviderState( loginMode = AsyncData.Failure(AN_EXCEPTION), eventSink = eventSink, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) } - private fun AndroidComposeTestRule.setChooseAccountProviderView( + private fun AndroidComposeUiTest.setChooseAccountProviderView( state: ChooseAccountProviderState, onBackClick: () -> Unit = EnsureNeverCalled(), - onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), + onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(), onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), @@ -93,7 +89,7 @@ class ChooseAccountProviderViewTest { ChooseAccountProviderView( state = state, onBackClick = onBackClick, - onOidcDetails = onOidcDetails, + onOAuthDetails = onOAuthDetails, onNeedLoginPassword = onNeedLoginPassword, onLearnMoreClick = onLearnMoreClick, onCreateAccountContinue = onCreateAccountContinue, 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 index 017fd1b633..805e3c10b6 100644 --- 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 @@ -10,7 +10,6 @@ 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 @@ -26,16 +25,15 @@ 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.robolectric.RobolectricTest 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 { +// Use RobolectricTest for the test with the Bitmap. +class ClassicFlowNodeHelperTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 6372841250..a9045ab152 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -22,9 +22,9 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow -import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow +import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest @@ -74,7 +74,7 @@ class ConfirmAccountProviderPresenterTest { fun `present - continue oidc`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) val presenter = createConfirmAccountProviderPresenter( @@ -89,21 +89,21 @@ class ConfirmAccountProviderPresenterTest { val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @Test - fun `present - oidc - cancel with failure`() = runTest { + fun `present - OAuth - cancel with failure`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) - val defaultOidcActionFlow = FakeOidcActionFlow() + val defaultOAuthActionFlow = FakeOAuthActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + defaultOAuthActionFlow = defaultOAuthActionFlow, ) presenter.test { val initialState = awaitItem() @@ -114,25 +114,25 @@ class ConfirmAccountProviderPresenterTest { val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) - authenticationService.givenOidcCancelError(AN_EXCEPTION) - defaultOidcActionFlow.post(OidcAction.GoBack()) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + authenticationService.givenOAuthCancelError(AN_EXCEPTION) + defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFailureState = awaitItem() assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @Test - fun `present - oidc - cancel with success`() = runTest { + fun `present - OAuth - cancel with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) - val defaultOidcActionFlow = FakeOidcActionFlow() + val defaultOAuthActionFlow = FakeOAuthActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + defaultOAuthActionFlow = defaultOAuthActionFlow, ) presenter.test { val initialState = awaitItem() @@ -143,24 +143,24 @@ class ConfirmAccountProviderPresenterTest { val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) - defaultOidcActionFlow.post(OidcAction.GoBack()) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFinalState = awaitItem() assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @Test - fun `present - oidc - cancel to unblock`() = runTest { + fun `present - OAuth - cancel to unblock`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) - val defaultOidcActionFlow = FakeOidcActionFlow() + val defaultOAuthActionFlow = FakeOAuthActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + defaultOAuthActionFlow = defaultOAuthActionFlow, ) presenter.test { val initialState = awaitItem() @@ -168,23 +168,23 @@ class ConfirmAccountProviderPresenterTest { val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - defaultOidcActionFlow.post(OidcAction.GoBack(toUnblock = true)) + defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) val cancelFinalState = awaitItem() assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @Test - fun `present - oidc - success with failure`() = runTest { + fun `present - OAuth - success with failure`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) - val defaultOidcActionFlow = FakeOidcActionFlow() + val defaultOAuthActionFlow = FakeOAuthActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + defaultOAuthActionFlow = defaultOAuthActionFlow, ) presenter.test { val initialState = awaitItem() @@ -195,9 +195,9 @@ class ConfirmAccountProviderPresenterTest { val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenLoginError(AN_EXCEPTION) - defaultOidcActionFlow.post(OidcAction.Success("aUrl")) + defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) val cancelLoadingState = awaitItem() assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val cancelFailureState = awaitItem() @@ -206,16 +206,16 @@ class ConfirmAccountProviderPresenterTest { } @Test - fun `present - oidc - success with success`() = runTest { + fun `present - OAuth - success with success`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) - val defaultOidcActionFlow = FakeOidcActionFlow() + val defaultOidcActionFlow = FakeOAuthActionFlow() val presenter = createConfirmAccountProviderPresenter( matrixAuthenticationService = authenticationService, - defaultOidcActionFlow = defaultOidcActionFlow, + defaultOAuthActionFlow = defaultOidcActionFlow, ) presenter.test { val initialState = awaitItem() @@ -226,8 +226,8 @@ class ConfirmAccountProviderPresenterTest { val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) - defaultOidcActionFlow.post(OidcAction.Success("aUrl")) + assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) val successSuccessState = awaitItem() assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java) } @@ -311,10 +311,10 @@ class ConfirmAccountProviderPresenterTest { } @Test - fun `present - confirm account creation with oidc is successful`() = runTest { + fun `present - confirm account creation with OAuth is successful`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) val presenter = createConfirmAccountProviderPresenter( @@ -327,16 +327,16 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading val submittedState = awaitItem() assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @Test - fun `present - confirm account creation with oidc and url continues with oidc`() = runTest { + fun `present - confirm account creation with OAuth and url continues with OAuth`() = runTest { val aUrl = "aUrl" val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails(supportsOidcLogin = true)) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, ) val presenter = createConfirmAccountProviderPresenter( @@ -350,12 +350,12 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading val submittedState = awaitItem() assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.Oidc::class.java) + assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @Test - fun `present - confirm account creation without oidc and with url continuing with url`() = runTest { + fun `present - confirm account creation without OAuth and with url continuing with url`() = runTest { val aUrl = "aUrl" val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { @@ -380,14 +380,14 @@ class ConfirmAccountProviderPresenterTest { params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), - defaultOidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), + defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, loginHelper = createLoginHelper( authenticationService = matrixAuthenticationService, - oidcActionFlow = defaultOidcActionFlow, + oAuthActionFlow = defaultOAuthActionFlow, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt index 26da50da63..3a128f2e4f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt @@ -6,21 +6,23 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.loginpassword import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasText -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.onNodeWithTag 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 androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.test.A_PASSWORD import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.testtags.TestTags @@ -30,158 +32,153 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class LoginPasswordViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class LoginPasswordViewTest : RobolectricTest() { @Test - fun `clicking on back invoke back callback`() { + fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `changing login invokes the expected event`() { + fun `changing login invokes the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = rule.activity.getString(CommonStrings.common_username) - rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME) + val userNameHint = activity!!.getString(CommonStrings.common_username) + onNodeWithText(userNameHint).performTextInput(A_USER_NAME) eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin(A_USER_NAME) ) } @Test - fun `changing login removes new lines the expected event`() { + fun `changing login removes new lines the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = rule.activity.getString(CommonStrings.common_username) - rule.onNodeWithText(userNameHint).performTextInput("a\nb") + val userNameHint = activity!!.getString(CommonStrings.common_username) + onNodeWithText(userNameHint).performTextInput("a\nb") eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin("ab") ) } @Test - fun `clearing login invokes the expected event`() { + fun `clearing login invokes the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(A_USER_NAME), eventSink = eventsRecorder, ), ) - val a11yClear = rule.activity.getString(CommonStrings.action_clear) - rule.onNodeWithContentDescription(a11yClear).performClick() + val a11yClear = activity!!.getString(CommonStrings.action_clear) + onNodeWithContentDescription(a11yClear).performClick() eventsRecorder.assertSingle( LoginPasswordEvents.SetLogin("") ) } @Test - fun `changing password invokes the expected event`() { + fun `changing password invokes the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, ), ) - val userNameHint = rule.activity.getString(CommonStrings.common_password) - rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD) + val userNameHint = activity!!.getString(CommonStrings.common_password) + onNodeWithText(userNameHint).performTextInput(A_PASSWORD) eventsRecorder.assertSingle( LoginPasswordEvents.SetPassword(A_PASSWORD) ) } @Test - fun `reveal password makes the password visible`() { + fun `reveal password makes the password visible`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), eventSink = eventsRecorder, ), ) - rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + val resources = activity!!.resources // Show password - val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password) - rule.onNodeWithContentDescription(a11yShowPassword).performClick() - rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) + val a11yShowPassword = resources.getString(CommonStrings.a11y_show_password) + onNodeWithContentDescription(a11yShowPassword).performClick() + onNodeWithTag(TestTags.loginPassword.value).assert(hasText(A_PASSWORD)) // Hide password - val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password) - rule.onNodeWithContentDescription(a11yHidePassword).performClick() - rule.onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) + val a11yHidePassword = resources.getString(CommonStrings.a11y_hide_password) + onNodeWithContentDescription(a11yHidePassword).performClick() + onNodeWithTag(TestTags.loginPassword.value).assert(hasText("••••••••")) } @Test - fun `when login is empty, continue button is not enabled`() { + fun `when login is empty, continue button is not enabled`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), eventSink = eventsRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsNotEnabled() } @Test - fun `when password is empty, continue button is not enabled`() { + fun `when password is empty, continue button is not enabled`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME), eventSink = eventsRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsNotEnabled() + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsNotEnabled() } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Continue sends expected event`() { + fun `clicking on Continue sends expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLoginPasswordView( + setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), eventSink = eventsRecorder, ), ) - val continueStr = rule.activity.getString(CommonStrings.action_continue) - rule.onNodeWithText(continueStr).assertIsEnabled() - rule.clickOn(CommonStrings.action_continue) + val continueStr = activity!!.getString(CommonStrings.action_continue) + onNodeWithText(continueStr).assertIsEnabled() + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( LoginPasswordEvents.Submit ) } } -private fun AndroidComposeTestRule.setLoginPasswordView( +private fun AndroidComposeUiTest.setLoginPasswordView( state: LoginPasswordState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt index 6e67925444..337f2d659c 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/DefaultOnBoardingLogoResIdProviderTest.kt @@ -8,14 +8,12 @@ package io.element.android.features.login.impl.screens.onboarding -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultOnBoardingLogoResIdProviderTest { +class DefaultOnBoardingLogoResIdProviderTest : RobolectricTest() { @Test fun `when onboarding_logo resource does not exist, get() returns null`() { val context = InstrumentationRegistry.getInstrumentation().context 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 1fdfb7e070..8249694278 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 @@ -31,8 +31,8 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2 import io.element.android.libraries.matrix.test.A_LOGIN_HINT import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.oidc.api.OidcActionFlow -import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow +import io.element.android.libraries.oauth.api.OAuthActionFlow +import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -312,11 +312,11 @@ private fun createPresenter( ) fun createLoginHelper( - oidcActionFlow: OidcActionFlow = FakeOidcActionFlow(), + oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), ): LoginHelper = LoginHelper( - oidcActionFlow = oidcActionFlow, + oAuthActionFlow = oAuthActionFlow, authenticationService = authenticationService, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, ) 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 ad09445075..52d1667687 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 @@ -6,20 +6,23 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + 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.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runAndroidComposeUiTest 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 -import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -29,22 +32,15 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTestParameter import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestParameterInjector - -@RunWith(RobolectricTestParameterInjector::class) -class OnboardingViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class OnboardingViewTest : RobolectricTestParameter() { @Test - fun `when can create account - clicking on create account calls the expected callback`() { + fun `when can create account - clicking on create account calls the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canCreateAccount = true, showDeveloperSettings = false, @@ -52,40 +48,40 @@ class OnboardingViewTest { ), onCreateAccount = callback, ) - rule.clickOn(R.string.screen_onboarding_sign_up) + 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() + val developerSettingsText = activity!!.getString(CommonStrings.common_developer_options) + onNodeWithContentDescription(developerSettingsText).assertDoesNotExist() } } @Test - fun `when can go back - clicking on back calls the expected callback`() { + fun `when can go back - clicking on back calls the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( isAddingAccount = true, eventSink = eventSink, ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() { + fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = true, eventSink = eventSink, ), onSignInWithQrCode = callback, ) - rule.clickOn(R.string.screen_onboarding_sign_in_with_qr_code) + clickOn(R.string.screen_onboarding_sign_in_with_qr_code) } } @@ -95,10 +91,10 @@ class OnboardingViewTest { "can search account provider" to false, "cannot search account provider" to true, ) - ) { + ) = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = true, mustChooseAccountProvider = mustChooseAccountProvider, @@ -106,7 +102,7 @@ class OnboardingViewTest { ), onSignIn = callback, ) - rule.clickOn(R.string.screen_onboarding_sign_in_manually) + clickOn(R.string.screen_onboarding_sign_in_manually) } } @@ -116,10 +112,10 @@ class OnboardingViewTest { "can search account provider" to false, "cannot search account provider" to true, ) - ) { + ) = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canLoginWithQrCode = false, canCreateAccount = false, @@ -128,89 +124,89 @@ class OnboardingViewTest { ), onSignIn = callback, ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) } } @Test - fun `when sign in to pre defined account provider - clicking on button emits the expected event`() { + fun `when sign in to pre defined account provider - clicking on button emits the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", eventSink = eventSink, ), ) - val buttonText = rule.activity.getString(R.string.screen_onboarding_sign_in_to, "element.io") - rule.onNodeWithText(buttonText).performClick() + val buttonText = activity!!.getString(R.string.screen_onboarding_sign_in_to, "element.io") + onNodeWithText(buttonText).performClick() eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io")) } @Test - fun `when error is displayed - closing the dialog emits the expected event`() { + fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", loginMode = AsyncData.Failure(AN_EXCEPTION), eventSink = eventSink, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventSink.assertSingle(OnBoardingEvents.ClearError) } @Test - fun `clicking on report a problem calls the sign in callback`() { + fun `clicking on report a problem calls the sign in callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canReportBug = true, eventSink = eventSink, ), onReportProblem = callback, ) - val text = rule.activity.getString(CommonStrings.common_report_a_problem) - rule.onNodeWithText(text).assertExists() - rule.clickOn(CommonStrings.common_report_a_problem) + val text = activity!!.getString(CommonStrings.common_report_a_problem) + onNodeWithText(text).assertExists() + clickOn(CommonStrings.common_report_a_problem) } } @Test - fun `clicking on settings calls the developer settings callback`() { + fun `clicking on settings calls the developer settings callback`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( showDeveloperSettings = true, eventSink = eventSink, ), onDeveloperSettingsClick = callback, ) - val text = rule.activity.getString(CommonStrings.common_developer_options) - rule.onNodeWithContentDescription(text).performClick() + val text = activity!!.getString(CommonStrings.common_developer_options) + onNodeWithContentDescription(text).performClick() } } @Test - fun `cannot report a problem when the feature is disabled`() { + fun `cannot report a problem when the feature is disabled`() = runAndroidComposeUiTest { val eventSink = EventsRecorder(expectEvents = false) - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( canReportBug = false, eventSink = eventSink, ), ) - val text = rule.activity.getString(CommonStrings.common_report_a_problem) - rule.onNodeWithText(text).assertDoesNotExist() + val text = activity!!.getString(CommonStrings.common_report_a_problem) + onNodeWithText(text).assertDoesNotExist() } @Test - fun `when success PasswordLogin - the expected callback is invoked and the event is received`() { + fun `when success PasswordLogin - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() ensureCalledOnce { callback -> - rule.setOnboardingView( + setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.PasswordLogin), eventSink = eventSink, @@ -222,27 +218,27 @@ class OnboardingViewTest { } @Test - fun `when success Oidc - the expected callback is invoked and the event is received`() { + fun `when success Oidc - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - val oidcDetails = OidcDetails("aUrl") - ensureCalledOnceWithParam(oidcDetails) { callback -> - rule.setOnboardingView( + val oAuthDetails = OAuthDetails("aUrl") + ensureCalledOnceWithParam(oAuthDetails) { callback -> + setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.Oidc(oidcDetails)), + loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), eventSink = eventSink, ), - onOidcDetails = callback, + onOAuthDetails = callback, ) } eventSink.assertSingle(OnBoardingEvents.ClearError) } @Test - fun `when success AccountCreation - the expected callback is invoked and the event is received`() { + fun `when success AccountCreation - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { val eventSink = EventsRecorder() - val oidcDetails = OidcDetails("aUrl") - ensureCalledOnceWithParam(oidcDetails.url) { callback -> - rule.setOnboardingView( + val oAuthDetails = OAuthDetails("aUrl") + ensureCalledOnceWithParam(oAuthDetails.url) { callback -> + setOnboardingView( state = anOnBoardingState( loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), eventSink = eventSink, @@ -253,7 +249,7 @@ class OnboardingViewTest { eventSink.assertSingle(OnBoardingEvents.ClearError) } - private fun AndroidComposeTestRule.setOnboardingView( + private fun AndroidComposeUiTest.setOnboardingView( state: OnBoardingState, onBackClick: () -> Unit = EnsureNeverCalled(), onDeveloperSettingsClick: () -> Unit = EnsureNeverCalled(), @@ -261,7 +257,7 @@ class OnboardingViewTest { onSignIn: (Boolean) -> Unit = EnsureNeverCalledWithParam(), onCreateAccount: () -> Unit = EnsureNeverCalled(), onReportProblem: () -> Unit = EnsureNeverCalled(), - onOidcDetails: (OidcDetails) -> Unit = EnsureNeverCalledWithParam(), + onOAuthDetails: (OAuthDetails) -> Unit = EnsureNeverCalledWithParam(), onNeedLoginPassword: () -> Unit = EnsureNeverCalled(), onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onCreateAccountContinue: (url: String) -> Unit = EnsureNeverCalledWithParam(), @@ -275,7 +271,7 @@ class OnboardingViewTest { onSignIn = onSignIn, onCreateAccount = onCreateAccount, onReportProblem = onReportProblem, - onOidcDetails = onOidcDetails, + onOAuthDetails = onOAuthDetails, onNeedLoginPassword = onNeedLoginPassword, onLearnMoreClick = onLearnMoreClick, onCreateAccountContinue = onCreateAccountContinue, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt index a0469a684e..8c1802a3c5 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationViewTest.kt @@ -6,49 +6,45 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.confirmation 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class QrCodeConfirmationViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class QrCodeConfirmationViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeConfirmationView( + setQrCodeConfirmationView( step = QrCodeConfirmationStep.DisplayCheckCode("12"), onCancel = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on Cancel button clicked - calls the expected callback`() { + fun `on Cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeConfirmationView( + setQrCodeConfirmationView( step = QrCodeConfirmationStep.DisplayVerificationCode("123456"), onCancel = callback ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeTestRule.setQrCodeConfirmationView( + private fun AndroidComposeUiTest.setQrCodeConfirmationView( step: QrCodeConfirmationStep, onCancel: () -> Unit ) { diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt index de0f689220..976d98c594 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorViewTest.kt @@ -6,59 +6,55 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.error 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.login.impl.qrcode.QrCodeErrorScreenType import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class QrCodeErrorViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class QrCodeErrorViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the onCancel callback`() { + fun `on back pressed - calls the onCancel callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeErrorView( + setQrCodeErrorView( onCancel = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on try again button clicked - calls the expected callback`() { + fun `on try again button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeErrorView( + setQrCodeErrorView( onRetry = callback, ) - rule.clickOn(CommonStrings.action_try_again) + clickOn(CommonStrings.action_try_again) } } @Test - fun `on cancel button clicked - calls the expected callback`() { + fun `on cancel button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeErrorView( + setQrCodeErrorView( onCancel = callback, ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } - private fun AndroidComposeTestRule.setQrCodeErrorView( + private fun AndroidComposeUiTest.setQrCodeErrorView( onRetry: () -> Unit = EnsureNeverCalled(), onCancel: () -> Unit = EnsureNeverCalled(), errorScreenType: QrCodeErrorScreenType = QrCodeErrorScreenType.UnknownError, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt index cec67e5011..5720c68ab4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.intro 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.login.impl.R import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder @@ -19,42 +21,36 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class QrCodeIntroViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class QrCodeIntroViewTest : RobolectricTest() { @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(), onBackClicked = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on back button clicked - calls the expected callback`() { + fun `on back button clicked - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(), onBackClicked = callback ) - rule.pressBack() + pressBack() } } @Test - fun `when can continue - calls the expected callback`() { + fun `when can continue - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(canContinue = true), onContinue = callback ) @@ -62,16 +58,16 @@ class QrCodeIntroViewTest { } @Test - fun `on submit button clicked - emits the Continue event`() { + fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { val eventRecorder = EventsRecorder() - rule.setQrCodeIntroView( + setQrCodeIntroView( state = aQrCodeIntroState(eventSink = eventRecorder), ) - rule.clickOn(R.string.screen_qr_code_login_initial_state_button_title) + clickOn(R.string.screen_qr_code_login_initial_state_button_title) eventRecorder.assertSingle(QrCodeIntroEvents.Continue) } - private fun AndroidComposeTestRule.setQrCodeIntroView( + private fun AndroidComposeUiTest.setQrCodeIntroView( state: QrCodeIntroState, onBackClicked: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt index b8becd545f..c0c7c084a8 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanViewTest.kt @@ -6,13 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.login.impl.screens.qrcode.scan import androidx.activity.ComponentActivity import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.test.platform.app.InstrumentationRegistry import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData @@ -22,18 +24,12 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class QrCodeScanViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class QrCodeScanViewTest : RobolectricTest() { private var provider: ProcessCameraProvider? = null @Before @@ -48,28 +44,28 @@ class QrCodeScanViewTest { } @Test - fun `on back pressed - calls the expected callback`() { + fun `on back pressed - calls the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setQrCodeScanView( + setQrCodeScanView( state = aQrCodeScanState(), onBackClick = callback ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `on QR code data ready - calls the expected callback`() { + fun `on QR code data ready - calls the expected callback`() = runAndroidComposeUiTest { val data = FakeMatrixQrCodeLoginData() ensureCalledOnceWithParam(data) { callback -> - rule.setQrCodeScanView( + setQrCodeScanView( state = aQrCodeScanState(authenticationAction = AsyncAction.Success(data)), onQrCodeDataReady = callback ) } } - private fun AndroidComposeTestRule.setQrCodeScanView( + private fun AndroidComposeUiTest.setQrCodeScanView( state: QrCodeScanState, onBackClick: () -> Unit = EnsureNeverCalled(), onQrCodeDataReady: (MatrixQrCodeLoginData) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index 8de7718980..d5356ced63 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.workmanager.api) api(projects.features.logout.api) diff --git a/features/logout/impl/src/main/res/values-be/translations.xml b/features/logout/impl/src/main/res/values-be/translations.xml index bc005e2bd2..0aab51833f 100644 --- a/features/logout/impl/src/main/res/values-be/translations.xml +++ b/features/logout/impl/src/main/res/values-be/translations.xml @@ -1,9 +1,9 @@ - "Вы ўпэўнены, што хочаце выйсці?" + "Вы ўпэўненыя, што хочаце выдаліць гэту прыладу?" "Выйсці" "Выйсці" - "Выхад…" + "Выдаленне прылады…" "Вы збіраецеся выйсці з апошняга сеанса. Калі вы выйдзеце з сістэмы зараз, вы страціце доступ да зашыфраваных паведамленняў." "Вы адключылі рэзервовае капіраванне" "Вашы ключы ўсё яшчэ захоўваліся, калі вы выйшлі з сеткі. Паўторна падключыцеся, каб можна было стварыць рэзервовую копію вашых ключоў перад выхадам." diff --git a/features/logout/impl/src/main/res/values-ca/translations.xml b/features/logout/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..eb0c82bb15 --- /dev/null +++ b/features/logout/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,17 @@ + + + "Segur que vols tancar sessió?" + "Tanca sessió" + "Tanca sessió" + "S\'està tancant la sessió…" + "Estàs a punt de tancar sessió. Si tanques sessió ara, perdràs l\'accés als missatges xifrats." + "Has desactivat la còpia de seguretat" + "Encara tens una còpia de seguretat de les teves claus i t\'has desconnectat. Torna a connectar-te per poder fer una còpia de seguretat de les teves claus abans de tancar la sessió." + "Encara s\'està fent una còpia de seguretat de les teves claus" + "Espera a que s\'hagi completat abans de tancar sessió." + "Encara s\'està fent una còpia de seguretat de les teves claus" + "Tanca sessió" + "Estàs a punt de tancar sessió a la teva última i única sessió. Si tanques sessió ara, perdràs l\'accés als missatges xifrats." + "Recuperació no configurada" + "Estàs a punt de tancar sessió. Si tanques sessió ara, pot ser que perdis l\'accés als missatges xifrats." + diff --git a/features/logout/impl/src/main/res/values-de/translations.xml b/features/logout/impl/src/main/res/values-de/translations.xml index 9021aee4da..0218642abc 100644 --- a/features/logout/impl/src/main/res/values-de/translations.xml +++ b/features/logout/impl/src/main/res/values-de/translations.xml @@ -4,15 +4,15 @@ "Dieses Gerät entfernen" "Dieses Gerät entfernen" "Gerät wird entfernt…" - "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." - "Du hast das Backup deaktiviert" - "Das Backup deiner Schlüssel lief noch, als du offline gegangen bist. Verbinde dich erneut, damit deine Schlüssel vor dem Abmelden gesichert werden können." + "Dies ist dein einziges Gerät. Wenn du es entfernst, benötigst du einen Wiederherstellungsschlüssel, um deine digitale Identität zu bestätigen und deine verschlüsselten Chats bei deiner nächsten Anmeldung wiederherzustellen." + "Du bist dabei, den Zugriff auf deine verschlüsselten Chats zu verlieren" + "Deine Schlüssel wurden noch gesichert, während du offline gegangen bist. Stelle die Verbindung wieder her, damit deine Schlüssel gesichert werden können, bevor du dieses Gerät entfernst." "Deine Schlüssel werden noch gesichert" "Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dieses Gerät entfernst." "Deine Schlüssel werden noch gesichert" "Dieses Gerät entfernen" - "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten." + "Dies ist dein einziges Gerät. Wenn du es entfernst, benötigst du einen Wiederherstellungsschlüssel, um deine digitale Identität zu bestätigen und deine verschlüsselten Chats bei deiner nächsten Anmeldung wiederherzustellen." "Du bist dabei, den Zugriff auf deine verschlüsselten Chats zu verlieren" - "Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du möglicherweise den Zugriff auf deine verschlüsselten Nachrichten." + "Dies ist dein einziges Gerät. Wenn du es entfernst, benötigst du einen Wiederherstellungsschlüssel, um deine digitale Identität zu bestätigen und deine verschlüsselten Chats bei deiner nächsten Anmeldung wiederherzustellen." "Stelle sicher, dass du Zugriff auf deinen Wiederherstellungsschlüssel hast, bevor du dieses Gerät entfernst" diff --git a/features/logout/impl/src/main/res/values-et/translations.xml b/features/logout/impl/src/main/res/values-et/translations.xml index ee5a2ce9d0..cd29e13354 100644 --- a/features/logout/impl/src/main/res/values-et/translations.xml +++ b/features/logout/impl/src/main/res/values-et/translations.xml @@ -1,18 +1,18 @@ - "Kas sa oled kindel, et soovid välja logida?" + "Kas sa oled kindel, et soovid selle seadme eemaldada?" "Eemalda see seade" "Eemalda see seade" - "Logime välja…" - "Oled oma viimasest seansist välja logimas. Kui logid nüüd välja, kaotad ligipääsu oma krüptitud sõnumitele." - "Sa oled varukoopiate tegemise välja lülitanud" - "Kui su võrguühendus katkes, siis sinu krüptovõtmed oli parasjagu varundamisel. Loo võrguühendus uuesti, oota kuni krüptovõtmete varundamine lõppeb ja alles siis logi rakendusest välja." + "Eemaldan seadet…" + "See on sinu ainus seade. Kui sa selle eemaldad, vajad taastamisvõtit, et kinnitada oma digitaalset identiteeti ja taastada järgmisel sisselogimisel oma krüptitud vestlused." + "Sa kaotad peagi juurdepääsu oma krüptitud vestlustele" + "Kui su võrguühendus katkes, siis sinu krüptovõtmed oli parasjagu varundamisel. Loo võrguühendus uuesti, oota kuni krüptovõtmete varundamine lõppeb ja alles siis eemalda see seade." "Sinu krüptovõtmed on veel varundamisel" - "Enne väljalogimist palun oota, et pooleliolev toiming lõppeb." + "Enne selle seadme eemaldamist palun oota, et pooleliolev toiming lõppeb." "Sinu krüptovõtmed on veel varundamisel" "Eemalda see seade" - "Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis kaotad ligipääsu oma krüptitud sõnumitele." + "See on sinu ainus seade. Kui sa selle eemaldad, vajad taastamisvõtit, et kinnitada oma digitaalset identiteeti ja taastada järgmisel sisselogimisel oma krüptitud vestlused." "Andmete taastamine on seadistamata" "Sa oled logimas välja oma viimasest sessioonist. Kui teed seda nüüd, siis ilmselt kaotad ligipääsu oma krüptitud sõnumitele." - "Kas sa oled oma taastevõtme salvestanud?" + "Enne selle seadme eemaldamist veendu, et sul on juurdepääs taastevõtmele" diff --git a/features/logout/impl/src/main/res/values-fa/translations.xml b/features/logout/impl/src/main/res/values-fa/translations.xml index 8dfaad8580..a540b9be37 100644 --- a/features/logout/impl/src/main/res/values-fa/translations.xml +++ b/features/logout/impl/src/main/res/values-fa/translations.xml @@ -1,18 +1,18 @@ - "مطمئنید که می‌خواهید از حسابتان خارج شوید؟" - "خروج" - "خروج" - "خارج شدن…" - "دارید از واپسین نشستتان خارج می‌شوید. اگر اکنون خارج شوید پیام‌های رمزنگاشته‌تان را از دست خواهید داد." - "پشتیبان را خاموش کرده‌اید" - "در هنگامی که آفلاین شدید، کلیدهای شما هنوز در حال پشتیبان‌گیری بودند. دوباره متصل شوید ، تا قبل از خروج از کلیدهایتان نسخه پشتیبان‌ گرفته شود." + "مطمئنید که می‌خواهید این افزاره را بردارید؟" + "برداشتن این افزاره" + "برداشتن این افزاره" + "برداشتن افزاره…" + "این تنها دستگاه شماست. اگر آن را جدا کنید، برای تأیید هویت دیجیتال خود و بازیابی چت‌های رمزگذاری شده‌تان در دفعه بعد که وارد سیستم می‌شوید، به یک کلید بازیابی نیاز خواهید داشت." + "شما در درحال از دست دادن دسترسی به چت‌های رمزگذاری‌شده‌تان هستید." + "وقتی آفلاین شدید، کلیدهای شما هنوز در حال پشتیبان‌گیری بودند. دوباره متصل شوید تا قبل از جدا کردن این دستگاه، از کلیدهایتان پشتیبان‌گیری شود." "کلیدهایتان هنوز در حال پشتیبان گیریند" - "لطفاً پیش از خروج منتظر پایانش شوید." + "لطفاً قبل از خروج از این دستگاه، منتظر بمانید تا این مراحل تکمیل شود." "کلیدهایتان هنوز در حال پشتیبان گیریند" - "خروج" - "شما در آستانه خروج از آخرین جلسه خود هستید. اگر اکنون از سیستم خارج شوید، دسترسی به پیام های رمزگذاری شده تان را از دست خواهید داد." - "بازگردانی برپا نشده" - "دارید از واپسین نشستتان خارج می‌شوید. اگر اکنون خارج شوید ممکن است پیام‌های رمزنگاشته‌تان را از دست بدهید." - "کلید بازیابیتان را ذخیره کرده‌اید؟" + "برداشتن این افزاره" + "این تنها دستگاه شماست. اگر آن را جدا کنید، برای تأیید هویت دیجیتال خود و بازیابی چت‌های رمزگذاری شده‌تان در دفعه بعد که وارد سیستم می‌شوید، به یک کلید بازیابی نیاز خواهید داشت." + "شما در حال از دست دادن دسترسی به چت‌های رمزگذاری‌شده‌تان هستید." + "این تنها دستگاه شماست. اگر آن را جدا کنید، برای تأیید هویت دیجیتال خود و بازیابی چت‌های رمزگذاری شده‌تان در دفعه بعد که وارد سیستم می‌شوید، به یک کلید بازیابی نیاز خواهید داشت." + "قبل از حذف این دستگاه، مطمئن شوید که به کلید بازیابی خود دسترسی دارید." diff --git a/features/logout/impl/src/main/res/values-in/translations.xml b/features/logout/impl/src/main/res/values-in/translations.xml index dabf83545e..9d63573973 100644 --- a/features/logout/impl/src/main/res/values-in/translations.xml +++ b/features/logout/impl/src/main/res/values-in/translations.xml @@ -1,16 +1,16 @@ - "Apakah Anda yakin ingin keluar dari akun?" - "Keluar dari akun" - "Keluar dari akun" - "Mengeluarkan dari akun…" + "Apakah Anda yakin ingin non aktifkan device dari akun?" + "Hapus device dari akun" + "Hapus device dari akun" + "Mengeluarkan device dari akun…" "Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda." "Anda telah menonaktifkan pencadangan" "Kunci Anda masih dicadangkan saat Anda luring. Sambungkan kembali sehingga kunci Anda dapat dicadangkan sebelum keluar." "Kunci Anda masih dicadangkan" "Mohon tunggu hingga proses ini selesai sebelum keluar." "Kunci Anda masih dicadangkan" - "Keluar dari akun" + "Hapus device dari akun" "Anda akan keluar dari sesi Anda yang terakhir. Jika Anda keluar sekarang, Anda akan kehilangan akses ke pesan terenkripsi Anda." "Pemulihan belum disiapkan" "Anda akan keluar dari sesi terakhir Anda. Jika Anda keluar sekarang, Anda mungkin kehilangan akses ke pesan terenkripsi Anda." diff --git a/features/logout/impl/src/main/res/values-pl/translations.xml b/features/logout/impl/src/main/res/values-pl/translations.xml index 46a5c2d6bd..691255f434 100644 --- a/features/logout/impl/src/main/res/values-pl/translations.xml +++ b/features/logout/impl/src/main/res/values-pl/translations.xml @@ -1,18 +1,18 @@ - "Czy na pewno chcesz się wylogować?" - "Wyloguj" - "Wyloguj" - "Wylogowywanie…" - "Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych." - "Wyłączyłeś backup" - "Twoje klucze były nadal archiwizowane po przejściu w tryb offline. Połącz się ponownie, aby zapisać w chmurze przed wylogowaniem." + "Czy na pewno chcesz usunąć to urządzenie?" + "Usuń to urządzenie" + "Usuń to urządzenie" + "Usuwam urządzenie…" + "To jest twoje jedyne urządzenie. Jeśli je usuniesz, będziesz potrzebować klucza przywracania, aby potwierdzić swoją tożsamość cyfrową i przywrócić zaszyfrowane czaty przy następnym logowaniu." + "Zamierzasz utracić dostęp do swoich zaszyfrowanych czatów" + "Twoje klucze były nadal archiwizowane po przejściu w tryb offline. Połącz się ponownie, aby zapisać je w chmurze przed usunięciem urządzenia." "Twoje klucze są nadal archiwizowane" - "Zanim się wylogujesz, poczekaj na zakończenie operacji." + "Poczekaj na zakończenie procesu, zanim usuniesz to urządzenie." "Twoje klucze są nadal archiwizowane" - "Wyloguj" - "Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych." - "Nie ustawiono przywracania" - "Zamierzasz wylogować się ze swojej ostatniej sesji. Jeśli wylogujesz się teraz, stracisz dostęp do swoich wiadomości szyfrowanych." - "Czy zapisałeś swój klucz przywracania?" + "Usuń to urządzenie" + "To jest twoje jedyne urządzenie. Jeśli je usuniesz, będziesz potrzebować klucza przywracania, aby potwierdzić swoją tożsamość cyfrową i przywrócić zaszyfrowane czaty przy następnym logowaniu." + "Zamierzasz utracić dostęp do swoich zaszyfrowanych czatów" + "To jest twoje jedyne urządzenie. Jeśli je usuniesz, będziesz potrzebować klucza przywracania, aby potwierdzić swoją tożsamość cyfrową i przywrócić zaszyfrowane czaty przy następnym logowaniu." + "Upewnij się, że posiadasz dostęp do klucza przywracania przed usunięciem urządzenia" diff --git a/features/logout/impl/src/main/res/values-pt/translations.xml b/features/logout/impl/src/main/res/values-pt/translations.xml index b8a7161c21..76e4f09d42 100644 --- a/features/logout/impl/src/main/res/values-pt/translations.xml +++ b/features/logout/impl/src/main/res/values-pt/translations.xml @@ -1,18 +1,18 @@ - "Tens a certeza que queres terminar a sessão?" - "Terminar sessão" - "Terminar sessão" - "A terminar sessão…" - "Estás prestes a terminar a tua última sessão. Se continuares, perderás o acesso às tuas mensagens cifradas." - "Desativaste a cópia de segurança" - "As tuas chaves ainda estavam a ser guardadas quando ficaste desligado. Volta a ligar-te para que as tuas chaves possam ser guardadas antes de encerrares a sessão." + "Tens a certeza que queres remover este dispositivo?" + "Remover este dispositivo" + "Remover este dispositivo" + "A remover dispositivo…" + "Este é o teu único dispositivo. Se o removeres, da próxima vez que iniciares sessão, precisarás da chave de recuperação para confirmares a tua identidade digital e recuperares as tuas conversas cifradas." + "Estás prestes a perder o acesso às tuas conversas privadas" + "As tuas chaves ainda estavam a ser guardadas quando ficaste desligado. Volta a ligar-te para que as tuas chaves possam ser guardadas antes de removeres o dispositivo." "As tuas chaves ainda estão a ser guardadas" - "Por favor, aguarda a conclusão desta operação antes de terminares a sessão." + "Por favor, aguarda a conclusão desta operação antes de removeres o dispositivo." "As tuas chaves ainda estão a ser guardadas" - "Terminar sessão" - "Estás prestes a terminar a tua última sessão. Se continuares, perderás o acesso às tuas mensagens cifradas." - "Recuperação não configurada" - "Estás prestes a terminar a tua última sessão. Se continuares, poderás perder o acesso às tuas mensagens cifradas." - "Guardaste a tua chave de recuperação?" + "Remover este dispositivo" + "Este é o teu único dispositivo. Se o removeres, da próxima vez que iniciares sessão, precisarás da chave de recuperação para confirmares a tua identidade digital e recuperares as tuas conversas cifradas." + "Estás prestes a perder o acesso às tuas conversas cifradas" + "Este é o teu único dispositivo. Se o removeres, da próxima vez que iniciares sessão, precisarás da chave de recuperação para confirmares a tua identidade digital e recuperares as tuas conversas cifradas." + "Certifica-te de que tens acesso à tua chave de recuperação antes de removeres este dispositivo" 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 d96b8f24f2..fde2aadcab 100644 --- a/features/logout/impl/src/main/res/values-ru/translations.xml +++ b/features/logout/impl/src/main/res/values-ru/translations.xml @@ -7,9 +7,9 @@ "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." "Вы потеряете доступ к своим зашифрованным чатам" "Когда вы отключились от сети, резервное копирование ваших ключей продолжалось. Подключитесь снова, чтобы резервная копия ваших ключей была создана, прежде чем вы отключите это устройство." - "Резервное копирование ключей все еще продолжается" + "Резервное копирование ключей всё ещё продолжается" "Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы." - "Резервное копирование ключей все еще продолжается" + "Резервное копирование ключей всё ещё продолжается" "Удалить это устройство" "Это ваше единственное устройство. Если вы его удалите, вам потребуется ключ восстановления, чтобы подтвердить свою цифровую личность и восстановить зашифрованные чаты при следующем входе в систему." "Вы потеряете доступ к своим зашифрованным чатам" diff --git a/features/logout/impl/src/main/res/values-sk/translations.xml b/features/logout/impl/src/main/res/values-sk/translations.xml index 39301437fb..28e0a05c2f 100644 --- a/features/logout/impl/src/main/res/values-sk/translations.xml +++ b/features/logout/impl/src/main/res/values-sk/translations.xml @@ -1,18 +1,18 @@ - "Ste si istí, že sa chcete odhlásiť?" - "Odhlásiť sa" - "Odhlásiť sa" - "Prebieha odhlasovanie…" + "Naozaj chcete odstrániť toto zariadenie?" + "Odstrániť toto zariadenie" + "Odstrániť toto zariadenie" + "Odoberanie zariadenia…" "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." "Vypli ste zálohovanie" "Keď ste sa odpojili od internetu, vaše kľúče sa ešte stále zálohovali. Pripojte sa znova k internetu, aby sa vaše kľúče mohli zálohovať pred odhlásením." "Vaše kľúče sa ešte stále zálohujú" "Pred odhlásením počkajte, kým sa to dokončí." "Vaše kľúče sa ešte stále zálohujú" - "Odhlásiť sa" + "Odstrániť toto zariadenie" "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." - "Obnovenie nie je nastavené" + "Čoskoro stratíte prístup k svojim zašifrovaným konverzáciám" "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." "Uložili ste si kľúč na obnovenie?" diff --git a/features/logout/impl/src/main/res/values-uk/translations.xml b/features/logout/impl/src/main/res/values-uk/translations.xml index 7e23189dc6..f012603533 100644 --- a/features/logout/impl/src/main/res/values-uk/translations.xml +++ b/features/logout/impl/src/main/res/values-uk/translations.xml @@ -1,9 +1,9 @@ - "Ви впевнені, що бажаєте вийти?" + "Ви впевнені, що хочете видалити цей пристрій?" "Вийти" "Вийти" - "Вихід…" + "Видалення пристрою…" "Ви збираєтеся вийти зі свого останнього сеансу. Якщо ви вийдете зараз, ви втратите доступ до своїх зашифрованих повідомлень." "Ви вимкнули резервне копіювання" "Коли ви вийшли з мережі, резервна копія ваших ключів все ще створювалася. Повторно під\'єднайтеся, щоб зберегти резервну копію ключів перед виходом." 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 3370b40f3c..f18904d03b 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/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt index 84ca038d7b..3fbae4bbc5 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.logout.impl 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings @@ -21,97 +23,92 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressTag -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class LogoutViewTest { - @get:Rule val rule = createAndroidComposeRule() +class LogoutViewTest : RobolectricTest() { @Test - fun `clicking on logout sends a LogoutEvents`() { + fun `clicking on logout sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_signout) + clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @Test - fun `confirming logout sends a LogoutEvents`() { + fun `confirming logout sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(LogoutEvents.Logout(false)) } @Test - fun `clicking on back invoke back callback`() { + fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLogoutView( + setLogoutView( aLogoutState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on confirm after error sends a LogoutEvents`() { + fun `clicking on confirm after error sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_signout_anyway) + clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(LogoutEvents.Logout(true)) } @Test - fun `clicking on cancel after error sends a LogoutEvents`() { + fun `clicking on cancel after error sends a LogoutEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLogoutView( + setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) } @Test - fun `last session setting button invoke onChangeRecoveryKeyClicked`() { + fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setLogoutView( + setLogoutView( aLogoutState( isLastDevice = true, eventSink = eventsRecorder ), onChangeRecoveryKeyClick = callback, ) - rule.clickOn(CommonStrings.common_settings) + clickOn(CommonStrings.common_settings) } } } -private fun AndroidComposeTestRule.setLogoutView( +private fun AndroidComposeUiTest.setLogoutView( state: LogoutState, onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt index 8eae534740..db8eed2a54 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.logout.impl.direct 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState @@ -20,84 +22,79 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Ignore -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class DefaultDirectLogoutViewTest { - @get:Rule val rule = createAndroidComposeRule() +class DefaultDirectLogoutViewTest : RobolectricTest() { @Test - fun `clicking on confirm logout sends expected Event`() { + fun `clicking on confirm logout sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_signout) + clickOn(CommonStrings.action_signout) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false)) } @Test - fun `clicking on cancel logout sends expected Event`() { + fun `clicking on cancel logout sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } @Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.") @Test - fun `clicking on back invoke back callback`() { + fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder, ) ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } @Test - fun `clicking on confirm after error sends expected Event`() { + fun `clicking on confirm after error sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_signout_anyway) + clickOn(CommonStrings.action_signout_anyway) eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true)) } @Test - fun `clicking on cancel after error sends expected Event`() { + fun `clicking on cancel after error sends expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDefaultDirectLogoutView( + setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) } } -private fun AndroidComposeTestRule.setDefaultDirectLogoutView( +private fun AndroidComposeUiTest.setDefaultDirectLogoutView( state: DirectLogoutState, ) { setContent { diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 6ff7f7e322..2661f7e330 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(libs.jsoup) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.exifinterface) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) implementation(libs.sigpwned.emoji4j) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvent.kt index bef8ca84d6..4d621e417f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvent.kt @@ -18,6 +18,8 @@ sealed interface MessagesEvent { data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvent data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvent data class OnUserClicked(val user: MatrixUser) : MessagesEvent + data object StopLiveLocationShare : MessagesEvent + data object ShowLiveLocationShare : MessagesEvent data object MarkAsFullyReadAndExit : MessagesEvent } 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 36e94ec456..7bb7ba1d00 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 @@ -11,6 +11,9 @@ package io.element.android.features.messages.impl import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe @@ -24,11 +27,12 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.annotations.ContributesNode -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.location.api.LocationService +import io.element.android.features.location.api.RenderingMapsNotSupportedDialog import io.element.android.features.location.api.ShareLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.location.api.ShowLocationMode @@ -53,6 +57,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.duration import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback @@ -121,6 +126,7 @@ class MessagesFlowNode( private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val dateFormatter: DateFormatter, private val coroutineDispatchers: CoroutineDispatchers, + private val hasVulkanSupport: DeviceHasVulkanSupport, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialTarget.toNavTarget(), @@ -143,6 +149,7 @@ class MessagesFlowNode( val mediaInfo: MediaInfo, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, + val canUseOverlay: Boolean, ) : NavTarget @Parcelize @@ -183,10 +190,15 @@ class MessagesFlowNode( @Parcelize data object ThreadsList : NavTarget + + @Parcelize + data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget } private val callback: MessagesEntryPoint.Callback = callback() + private var displayVulkanNotSupportedError by mutableStateOf(false) + override fun onBuilt() { super.onBuilt() lifecycle.subscribe( @@ -227,10 +239,11 @@ class MessagesFlowNode( callback.navigateToRoomDetails() } - override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { + override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean { return processEventClick( timelineMode = timelineMode, event = event, + canUseOverlay = canUseOverlay, ) } @@ -265,7 +278,11 @@ class MessagesFlowNode( } override fun navigateToSendLocation() { - backstack.push(NavTarget.SendLocation(Timeline.Mode.Live)) + if (hasVulkanSupport()) { + backstack.push(NavTarget.SendLocation(Timeline.Mode.Live)) + } else { + displayVulkanNotSupportedError = true + } } override fun navigateToCreatePoll() { @@ -276,14 +293,22 @@ class MessagesFlowNode( backstack.push(NavTarget.EditPoll(Timeline.Mode.Live, eventId)) } + override fun navigateToCurrentLiveLocation() { + if (hasVulkanSupport()) { + backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId))) + } else { + displayVulkanNotSupportedError = true + } + } + override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { - val callType = CallType.RoomCall( + val callData = CallData( sessionId = sessionId, roomId = roomId, - isAudioCall = isAudioCall + isAudioCall = isAudioCall, ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callType) + elementCallEntryPoint.startCall(callData) } override fun navigateToPinnedMessagesList() { @@ -305,6 +330,10 @@ class MessagesFlowNode( override fun navigateToDeveloperSettings() { callback.navigateToDeveloperSettings() } + + override fun navigateToAvatarPreview(username: String, avatarUrl: String) { + overlay.show(NavTarget.AvatarPreview(username, avatarUrl)) + } } val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId) createNode(buildContext, listOf(callback, inputs)) @@ -320,7 +349,11 @@ class MessagesFlowNode( ) val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() { - overlay.hide() + if (navTarget.canUseOverlay) { + overlay.hide() + } else { + backstack.pop() + } } override fun viewInTimeline(eventId: EventId) { @@ -414,10 +447,11 @@ class MessagesFlowNode( } NavTarget.PinnedMessagesList -> { val callback = object : PinnedMessagesListNode.Callback { - override fun handleEventClick(event: TimelineItem.Event) { + override fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) { processEventClick( timelineMode = Timeline.Mode.PinnedEvents, event = event, + canUseOverlay = canUseOverlay, ) } @@ -456,10 +490,11 @@ class MessagesFlowNode( focusedEventId = navTarget.focusedEventId, ) val callback = object : ThreadedMessagesNode.Callback { - override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean { + override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean { return processEventClick( timelineMode = timelineMode, event = event, + canUseOverlay = canUseOverlay, ) } @@ -494,7 +529,11 @@ class MessagesFlowNode( } override fun navigateToSendLocation() { - backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId))) + if (hasVulkanSupport()) { + backstack.push(NavTarget.SendLocation(Timeline.Mode.Thread(navTarget.threadRootId))) + } else { + displayVulkanNotSupportedError = true + } } override fun navigateToCreatePoll() { @@ -505,14 +544,22 @@ class MessagesFlowNode( backstack.push(NavTarget.EditPoll(Timeline.Mode.Thread(navTarget.threadRootId), eventId)) } + override fun navigateToCurrentLiveLocation() { + if (hasVulkanSupport()) { + backstack.push(NavTarget.LocationViewer(ShowLocationMode.Live(senderId = sessionId))) + } else { + displayVulkanNotSupportedError = true + } + } + override fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) { - val callType = CallType.RoomCall( + val callData = CallData( sessionId = sessionId, roomId = roomId, isAudioCall = isAudioCall ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(callType) + elementCallEntryPoint.startCall(callData) } override fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) { @@ -522,6 +569,10 @@ class MessagesFlowNode( override fun navigateToDeveloperSettings() { callback.navigateToDeveloperSettings() } + + override fun navigateToAvatarPreview(username: String, avatarUrl: String) { + overlay.show(NavTarget.AvatarPreview(username, avatarUrl)) + } } createNode(buildContext, listOf(inputs, callback)) } @@ -533,6 +584,31 @@ class MessagesFlowNode( } createNode(buildContext, listOf(callback)) } + is NavTarget.AvatarPreview -> { + val callback = object : MediaViewerEntryPoint.Callback { + override fun onDone() { + overlay.hide() + } + + override fun viewInTimeline(eventId: EventId) { + // Cannot happen + } + + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { + // Cannot happen + } + } + val params = mediaViewerEntryPoint.createParamsForAvatar( + filename = navTarget.name, + avatarUrl = navTarget.avatarUrl, + ) + mediaViewerEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + params = params, + callback = callback, + ) + } } } @@ -547,6 +623,7 @@ class MessagesFlowNode( private fun processEventClick( timelineMode: Timeline.Mode, event: TimelineItem.Event, + canUseOverlay: Boolean, ): Boolean { val navTarget = when (event.content) { is TimelineItemImageContent -> { @@ -556,6 +633,7 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, + canUseOverlay = canUseOverlay, ) } is TimelineItemVideoContent -> { @@ -565,6 +643,7 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, + canUseOverlay = canUseOverlay, ) } is TimelineItemFileContent -> { @@ -574,6 +653,7 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = event.content.thumbnailSource, + canUseOverlay = canUseOverlay, ) } is TimelineItemAudioContent -> { @@ -583,27 +663,37 @@ class MessagesFlowNode( content = event.content, mediaSource = event.content.mediaSource, thumbnailSource = null, + canUseOverlay = canUseOverlay, ) } is TimelineItemLocationContent -> { - val mode = when (event.content.mode) { - is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId) - is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static( - location = event.content.mode.location, - senderName = event.safeSenderName, - senderId = event.senderId, - senderAvatarUrl = event.senderAvatar.url, - timestamp = event.sentTimeMillis, - assetType = event.content.assetType, - ) + if (hasVulkanSupport()) { + val mode = when (event.content.mode) { + is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId) + is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static( + location = event.content.mode.location, + senderName = event.safeSenderName, + senderId = event.senderId, + senderAvatarUrl = event.senderAvatar.url, + timestamp = event.sentTimeMillis, + assetType = event.content.assetType, + ) + } + NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() } + } else { + displayVulkanNotSupportedError = true + null } - NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() } } else -> null } return when (navTarget) { is NavTarget.MediaViewer -> { - overlay.show(navTarget) + if (canUseOverlay) { + overlay.show(navTarget) + } else { + backstack.push(navTarget) + } true } is NavTarget.LocationViewer -> { @@ -620,6 +710,7 @@ class MessagesFlowNode( content: TimelineItemEventContentWithAttachment, mediaSource: MediaSource, thumbnailSource: MediaSource?, + canUseOverlay: Boolean, ): NavTarget { return NavTarget.MediaViewer( mode = mode, @@ -628,6 +719,7 @@ class MessagesFlowNode( filename = content.filename, fileSize = content.fileSize, caption = content.caption, + formattedCaption = content.formattedCaption, mimeType = content.mimeType, formattedFileSize = content.formattedFileSize, fileExtension = content.fileExtension, @@ -647,6 +739,7 @@ class MessagesFlowNode( ), mediaSource = mediaSource, thumbnailSource = thumbnailSource, + canUseOverlay = canUseOverlay, ) } @@ -663,6 +756,11 @@ class MessagesFlowNode( @Composable override fun View(modifier: Modifier) { mentionSpanTheme.updateStyles() + + if (displayVulkanNotSupportedError) { + RenderingMapsNotSupportedDialog { displayVulkanNotSupportedError = false } + } + CompositionLocalProvider( LocalMentionSpanUpdater provides mentionSpanUpdater ) { 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 e475f579c3..6113b68aab 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 @@ -26,5 +26,6 @@ interface MessagesNavigator { fun navigateToMember(userId: UserId) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) fun navigateToDeveloperSettings() + fun navigateToCurrentLiveLocation() fun close() } 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 a2cf4a3da0..47516a6cde 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 @@ -66,8 +66,11 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.finishLongRunningTransaction @@ -105,7 +108,7 @@ class MessagesNode( private val timelineController = TimelineController(room, room.liveTimeline) private val presenter = presenterFactory.create( navigator = this, - composerPresenter = messageComposerPresenterFactory.create(timelineController, this, isInThread = false), + composerPresenter = messageComposerPresenterFactory.create(timelineController, this, threadRoot = null), timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), actionListPresenter = actionListPresenterFactory.create( postProcessor = TimelineItemActionPostProcessor.Default, @@ -115,7 +118,7 @@ class MessagesNode( ) interface Callback : Plugin { - fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean + fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData) @@ -125,6 +128,7 @@ class MessagesNode( fun navigateToSendLocation() fun navigateToCreatePoll() fun navigateToEditPoll(eventId: EventId) + fun navigateToCurrentLiveLocation() fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) fun navigateToRoomDetails() @@ -133,6 +137,8 @@ class MessagesNode( fun navigateToDeveloperSettings() fun navigateToThreadsList() + + fun navigateToAvatarPreview(username: String, avatarUrl: String) } override fun onBuilt() { @@ -237,6 +243,10 @@ class MessagesNode( callback.navigateToDeveloperSettings() } + override fun navigateToCurrentLiveLocation() { + callback.navigateToCurrentLiveLocation() + } + private fun displaySameRoomToast() { context.toast(CommonStrings.screen_room_permalink_same_room_android) } @@ -247,6 +257,7 @@ class MessagesNode( override fun View(modifier: Modifier) { val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() + val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -268,11 +279,11 @@ class MessagesNode( onRoomDetailsClick = callback::navigateToRoomDetails, onEventContentClick = { isLive, event -> if (isLive) { - callback.handleEventClick(timelineController.mainTimelineMode(), event) + callback.handleEventClick(timelineController.mainTimelineMode(), event, canUseOverlay) } else { val detachedTimelineMode = timelineController.detachedTimelineMode() if (detachedTimelineMode != null) { - callback.handleEventClick(detachedTimelineMode, event) + callback.handleEventClick(detachedTimelineMode, event, canUseOverlay) } else { false } @@ -311,6 +322,11 @@ class MessagesNode( else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, + onAvatarClick = { user -> + user.avatarUrl?.let { url -> + callback.navigateToAvatarPreview(user.getBestName(), url) + } + }, modifier = Modifier, ) 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 b98bd764de..d3737f5fae 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 @@ -27,6 +27,8 @@ import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.PinUnpinAction import io.element.android.appconfig.MessageComposerConfig +import io.element.android.features.location.api.live.ActiveLiveLocationShareManager +import io.element.android.features.location.api.live.isCurrentlySharing 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 @@ -77,8 +79,8 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.ui.messages.reply.map import io.element.android.libraries.matrix.ui.model.getAvatarData @@ -126,6 +128,7 @@ class MessagesPresenter( private val featureFlagService: FeatureFlagService, private val addRecentEmoji: AddRecentEmoji, private val markAsFullyRead: MarkAsFullyRead, + private val liveLocationShareManager: ActiveLiveLocationShareManager, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory @@ -172,6 +175,7 @@ class MessagesPresenter( } val canOpenThreadList by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomThreadList).collectAsState(initial = false) + val isCurrentlySharingLiveLocationInRoom by remember { liveLocationShareManager.isCurrentlySharing(room.roomId) }.collectAsState() val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms -> perms.userEventPermissions() @@ -260,6 +264,18 @@ class MessagesPresenter( is MessagesEvent.OnUserClicked -> { roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) } + MessagesEvent.StopLiveLocationShare -> { + localCoroutineScope.launch { + liveLocationShareManager.stopShare(room.roomId) + .onFailure { + Timber.e(it, "Failed to stop live location share for roomId=${room.roomId}") + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) + } + } + } + MessagesEvent.ShowLiveLocationShare -> { + navigator.navigateToCurrentLiveLocation() + } is MessagesEvent.MarkAsFullyReadAndExit -> if (!markingAsReadAndExiting.getAndSet(true)) { coroutineScope.launch { val latestEventId = room.liveTimeline.getLatestEventId().getOrElse { @@ -272,6 +288,8 @@ class MessagesPresenter( } } navigator.close() + }.invokeOnCompletion { + markingAsReadAndExiting.set(false) } } } @@ -309,6 +327,7 @@ class MessagesPresenter( // TODO calculate this properly based on the thread list and the read state of each thread hasUnreadThreads = false, ), + showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom && timelineState.timelineMode !is Timeline.Mode.Thread, eventSink = ::handleEvent, ) } @@ -531,7 +550,7 @@ class MessagesPresenter( val replyToDetails = loadReplyDetails(targetEvent.eventId).map(permalinkParser) val composerMode = MessageComposerMode.Reply( replyToDetails = replyToDetails, - hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId), + hideImage = timelineProtectionState.hideMediaContent(targetEvent.eventId, targetEvent.isMine), ) composerState.eventSink( MessageComposerEvent.SetMode(composerMode) 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 862f30832b..a16485c6f7 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,6 +58,7 @@ data class MessagesState( val topBarSharedHistoryIcon: SharedHistoryIcon, val successorRoom: SuccessorRoom?, val threads: Threads, + val showLiveLocationShareBanner: Boolean, val eventSink: (MessagesEvent) -> Unit ) { val isTombstoned = successorRoom != null 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 16021df3e9..6389089e07 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 @@ -44,6 +44,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration import io.element.android.libraries.architecture.AsyncData 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.ROOM_NAME 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.encryption.identity.IdentityState @@ -79,6 +80,7 @@ open class MessagesStateProvider : PreviewParameterProvider { currentPinnedMessageIndex = 0, ), ), + aMessagesState(isCurrentlySharingLiveLocationInRoom = true), aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)), aMessagesState( timelineState = aTimelineState( @@ -94,8 +96,8 @@ open class MessagesStateProvider : PreviewParameterProvider { } fun aMessagesState( - roomName: String? = "Room name", - roomAvatar: AvatarData = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom), + roomName: String? = ROOM_NAME, + roomAvatar: AvatarData = AvatarData("!id:domain", ROOM_NAME, size = AvatarSize.TimelineRoom), userEventPermissions: UserEventPermissions = aUserEventPermissions(), composerState: MessageComposerState = aMessageComposerState( textEditorState = aTextEditorStateRich(initialText = "Hello", initialFocus = true), @@ -126,6 +128,7 @@ fun aMessagesState( hasThreads = false, hasUnreadThreads = false, ), + isCurrentlySharingLiveLocationInRoom: Boolean = false, eventSink: (MessagesEvent) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -155,6 +158,7 @@ fun aMessagesState( topBarSharedHistoryIcon = topBarSharedHistoryIcon, successorRoom = successorRoom, threads = threads, + showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom, 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 5a0b14b820..b299ea42a9 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 @@ -17,6 +17,7 @@ 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.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets @@ -56,6 +57,7 @@ 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.location.api.LiveLocationSharingBanner 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 @@ -205,15 +207,15 @@ fun MessagesView( val expandableState = rememberExpandableBottomSheetLayoutState() ExpandableBottomSheetLayout( modifier = modifier - .fillMaxSize() - .imePadding() - .systemBarsPadding() - .onSizeChanged { size -> - // Let the composer takes at max half of the available height. - // The value will be different if the soft keyboard is displayed - // or not. - maxComposerHeightPx = (size.height * 0.5f).toInt() - }, + .fillMaxSize() + .imePadding() + .systemBarsPadding() + .onSizeChanged { size -> + // Let the composer takes at max half of the available height. + // The value will be different if the soft keyboard is displayed + // or not. + maxComposerHeightPx = (size.height * 0.5f).toInt() + }, content = { Scaffold( contentWindowInsets = WindowInsets.statusBars, @@ -250,8 +252,8 @@ fun MessagesView( content = { padding -> Box( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) { MessagesViewContent( state = state, @@ -288,10 +290,10 @@ fun MessagesView( SuggestionsPickerView( modifier = Modifier - .shadow(10.dp) - .background(ElementTheme.colors.bgCanvasDefault) - .align(Alignment.BottomStart) - .heightIn(max = 230.dp), + .shadow(10.dp) + .background(ElementTheme.colors.bgCanvasDefault) + .align(Alignment.BottomStart) + .heightIn(max = 230.dp), roomId = state.roomId, roomName = state.roomName, roomAvatarData = state.roomAvatar, @@ -411,7 +413,7 @@ fun MessagesView( } @Composable -internal fun MessagesMenuActions( +internal fun RowScope.MessagesMenuActions( displayThreads: Boolean, roomCallState: RoomCallState, onJoinCallClick: (isAudioCall: Boolean) -> Unit, @@ -467,9 +469,9 @@ private fun MessagesViewContent( ) { Box( modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding(), + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), ) { AttachmentsBottomSheet( state = state.composerState, @@ -520,25 +522,34 @@ private fun MessagesViewContent( ) 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(), - ) { - fun focusOnPinnedEvent(eventId: EventId) { - state.timelineState.eventSink( - TimelineEvent.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds) + Column { + AnimatedVisibility( + visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, + modifier = Modifier.onSizeChanged { pinnedBannerHeightDp = with(density) { it.height.toDp() } }, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + fun focusOnPinnedEvent(eventId: EventId) { + state.timelineState.eventSink( + TimelineEvent.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds) + ) + } + PinnedMessagesBannerView( + state = state.pinnedMessagesBannerState, + onClick = ::focusOnPinnedEvent, + onViewAllClick = onViewAllPinnedMessagesClick, + ) + } + if (state.showLiveLocationShareBanner) { + LiveLocationSharingBanner( + onClick = { state.eventSink(MessagesEvent.ShowLiveLocationShare) }, + onStopClick = { state.eventSink(MessagesEvent.StopLiveLocationShare) } ) } - PinnedMessagesBannerView( - state = state.pinnedMessagesBannerState, - onClick = ::focusOnPinnedEvent, - onViewAllClick = onViewAllPinnedMessagesClick, - ) } - knockRequestsBannerView() } + + knockRequestsBannerView() } } } @@ -587,9 +598,9 @@ private fun MessagesViewComposerBottomSheetContents( private fun CantSendMessageBanner() { Row( modifier = Modifier - .fillMaxWidth() - .background(ElementTheme.colors.bgSubtleSecondary) - .padding(16.dp), + .fillMaxWidth() + .background(ElementTheme.colors.bgSubtleSecondary) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index a69c6d7612..56bac5be33 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions +import io.element.android.features.messages.impl.timeline.model.event.aStaticLocationMode import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent @@ -127,7 +128,7 @@ open class ActionListStateProvider : PreviewParameterProvider { anActionListState( target = ActionListState.Target.Success( event = aTimelineItemEvent( - content = aTimelineItemLocationContent(), + content = aTimelineItemLocationContent(mode = aStaticLocationMode()), timelineItemReactions = reactionsState ), sentTimeFull = "January 1, 1970 at 12:00 AM", @@ -140,7 +141,7 @@ open class ActionListStateProvider : PreviewParameterProvider { anActionListState( target = ActionListState.Target.Success( event = aTimelineItemEvent( - content = aTimelineItemLocationContent(), + content = aTimelineItemLocationContent(mode = aStaticLocationMode()), timelineItemReactions = reactionsState ), sentTimeFull = "January 1, 1970 at 12:00 AM", diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index e10dd2e1bc..c2d97dcf80 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -249,7 +249,7 @@ private fun ActionListViewContent( leadingContent = ListItemContent.Icon(IconSource.Resource(action.icon)), style = when { action.destructive -> ListItemStyle.Destructive - else -> ListItemStyle.Primary + else -> ListItemStyle.Default } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt index d989b34ab3..73fa55228d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt @@ -16,5 +16,12 @@ import kotlinx.parcelize.Parcelize @Immutable sealed interface Attachment : Parcelable { @Parcelize - data class Media(val localMedia: LocalMedia) : Attachment + data class Media( + val localMedia: LocalMedia, + // When true, the media was picked through the "Files" picker and should be + // uploaded without image recompression; videos still use the highest available + // / best-fit preset rather than an additional size-reduction optimization pass. + // See https://github.com/element-hq/element-x-android/issues/6365 + val sendAsFile: Boolean = false, + ) : Attachment } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentMediaInfo.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentMediaInfo.kt new file mode 100644 index 0000000000..7feeff18dd --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentMediaInfo.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.attachments.preview + +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAnimatedImage +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage +import io.element.android.libraries.mediaviewer.api.MediaInfo +import java.util.Locale + +internal fun MediaInfo.canEditImage(): Boolean { + val resolvedMimeType = resolvedImageMimeType() ?: return false + return resolvedMimeType.isMimeTypeImage() && + !resolvedMimeType.isMimeTypeAnimatedImage() && + resolvedMimeType != MimeTypes.Svg +} + +internal fun MediaInfo.isImageAttachment(): Boolean { + return resolvedImageMimeType().isMimeTypeImage() +} + +internal fun MediaInfo.resolvedImageMimeType(): String? { + return mimeType.takeIf { it.isMimeTypeImage() } ?: fileExtension.toImageMimeTypeOrNull() +} + +private fun String.toImageMimeTypeOrNull(): String? { + return when (lowercase(Locale.ROOT)) { + "png" -> MimeTypes.Png + "jpg", "jpeg" -> MimeTypes.Jpeg + "gif" -> MimeTypes.Gif + "webp" -> MimeTypes.WebP + "svg" -> MimeTypes.Svg + "bmp" -> "image/bmp" + "heic" -> "image/heic" + "heif" -> "image/heif" + "avif" -> "image/avif" + else -> null + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt index d473d4c3f4..fb9fddbb48 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvent.kt @@ -8,8 +8,19 @@ package io.element.android.features.messages.impl.attachments.preview +import io.element.android.features.messages.impl.attachments.preview.imageeditor.NormalizedCropRect + sealed interface AttachmentsPreviewEvent { data object SendAttachment : AttachmentsPreviewEvent data object CancelAndDismiss : AttachmentsPreviewEvent data object CancelAndClearSendState : AttachmentsPreviewEvent + data object OpenImageEditor : AttachmentsPreviewEvent + data object CloseImageEditor : AttachmentsPreviewEvent + data object RotateImageToTheLeft : AttachmentsPreviewEvent + data object FlipImageHorizontally : AttachmentsPreviewEvent + data object FlipImageVertically : AttachmentsPreviewEvent + data object ApplyImageEdits : AttachmentsPreviewEvent + data object ResetImageEdits : AttachmentsPreviewEvent + data class UpdateImageCropRect(val cropRect: NormalizedCropRect) : AttachmentsPreviewEvent + data object ClearImageEditError : AttachmentsPreviewEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 8e92e53f6a..0f60d7ff0e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -22,7 +22,12 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEditor +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEditorState +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEdits import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorPresenter +import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState +import io.element.android.features.messages.impl.attachments.video.VideoCompressionPresetSelector import io.element.android.libraries.androidutils.file.TemporaryUriDeleter import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.hash.hash @@ -30,7 +35,6 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.firstInstanceOf import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.EventId @@ -49,7 +53,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber +import java.io.File @AssistedInject class AttachmentsPreviewPresenter( @@ -60,7 +66,9 @@ class AttachmentsPreviewPresenter( mediaSenderFactory: MediaSenderFactory, private val permalinkBuilder: PermalinkBuilder, private val temporaryUriDeleter: TemporaryUriDeleter, + private val attachmentImageEditor: AttachmentImageEditor, private val mediaOptimizationSelectorPresenterFactory: MediaOptimizationSelectorPresenter.Factory, + private val videoCompressionPresetSelector: VideoCompressionPresetSelector, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, @@ -84,6 +92,14 @@ class AttachmentsPreviewPresenter( val sendActionState = remember { mutableStateOf(SendActionState.Idle) } + val originalLocalMedia = remember { (attachment as Attachment.Media).localMedia } + var currentAttachment by remember { mutableStateOf(attachment) } + var canEditImage by remember { mutableStateOf(originalLocalMedia.info.canEditImage()) } + var imageEditorState by remember { mutableStateOf(null) } + var appliedImageEdits by remember { mutableStateOf(AttachmentImageEdits()) } + var isApplyingImageEdits by remember { mutableStateOf(false) } + var displayImageEditError by remember { mutableStateOf(false) } + var editedTempFile by remember { mutableStateOf(null) } val markdownTextEditorState = rememberMarkdownTextEditorState(initialText = null, initialFocus = false) val textEditorState by rememberUpdatedState( @@ -94,9 +110,12 @@ class AttachmentsPreviewPresenter( var preprocessMediaJob by remember { mutableStateOf(null) } - val mediaAttachment = attachment as Attachment.Media + val mediaAttachment = currentAttachment as Attachment.Media val mediaOptimizationSelectorPresenter = remember { - mediaOptimizationSelectorPresenterFactory.create(mediaAttachment.localMedia) + mediaOptimizationSelectorPresenterFactory.create( + localMedia = mediaAttachment.localMedia, + sendAsFile = mediaAttachment.sendAsFile, + ) } val mediaOptimizationSelectorState by rememberUpdatedState(mediaOptimizationSelectorPresenter.present()) @@ -104,24 +123,46 @@ class AttachmentsPreviewPresenter( var displayFileTooLargeError by remember { mutableStateOf(false) } - LaunchedEffect(mediaOptimizationSelectorState.displayMediaSelectorViews) { + LaunchedEffect( + mediaOptimizationSelectorState.displayMediaSelectorViews, + mediaOptimizationSelectorState.videoSizeEstimations, + currentAttachment, + imageEditorState, + isApplyingImageEdits, + ) { // If the media optimization selector is not displayed, we can pre-process the media // to prepare it for sending. This is done to avoid blocking the UI thread when the // user clicks on the send button. - if (mediaOptimizationSelectorState.displayMediaSelectorViews == false) { - preprocessMediaJob = preProcessAttachment( - attachment = attachment, - mediaOptimizationConfig = mediaOptimizationConfigProvider.get(), + @Suppress("ComplexCondition") + if (mediaOptimizationSelectorState.displayMediaSelectorViews == false && + preprocessMediaJob == null && + imageEditorState == null && + !isApplyingImageEdits) { + if (mediaAttachment.localMedia.info.mimeType.isMimeTypeVideo() && mediaOptimizationSelectorState.videoSizeEstimations.dataOrNull() == null) { + Timber.d("Waiting for video size estimations to be able to select the best video compression preset before pre-processing the media") + return@LaunchedEffect + } + val config = getAutoPreprocessMediaOptimizationConfig( + mediaAttachment = mediaAttachment, + mediaOptimizationSelectorState = mediaOptimizationSelectorState, + ) ?: return@LaunchedEffect + preprocessMediaJob = coroutineScope.preProcessAttachment( + attachment = currentAttachment, + mediaOptimizationConfig = config, displayProgress = false, sendActionState = sendActionState, ) } } + LaunchedEffect(originalLocalMedia) { + canEditImage = originalLocalMedia.info.canEditImage() || attachmentImageEditor.canEdit(originalLocalMedia) + } + val maxUploadSize = mediaOptimizationSelectorState.maxUploadSize.dataOrNull() LaunchedEffect(maxUploadSize) { // Check file upload size if the media won't be processed for upload - val isImageFile = mediaAttachment.localMedia.info.mimeType.isMimeTypeImage() + val isImageFile = mediaAttachment.localMedia.info.isImageAttachment() val isVideoFile = mediaAttachment.localMedia.info.mimeType.isMimeTypeVideo() if (maxUploadSize != null && !(isImageFile || isVideoFile)) { // If file size is not known, we're permissive and allow sending. The SDK will cancel the upload if needed. @@ -152,7 +193,7 @@ class AttachmentsPreviewPresenter( videoCompressionPreset = mediaOptimizationSelectorState.selectedVideoPreset ?: VideoCompressionPreset.STANDARD, ) preprocessMediaJob = preProcessAttachment( - attachment = attachment, + attachment = currentAttachment, mediaOptimizationConfig = config, displayProgress = true, sendActionState = sendActionState, @@ -171,6 +212,9 @@ class AttachmentsPreviewPresenter( val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder) .takeIf { it.isNotEmpty() } + val editedTempFileToDelete = editedTempFile + editedTempFile = null + // If we're supposed to send the media as a background job, we can dismiss this screen already if (coroutineContext.isActive) { onDoneListener() @@ -178,33 +222,36 @@ class AttachmentsPreviewPresenter( // Send the media using the session coroutine scope so it doesn't matter if this screen or the chat one are closed sessionCoroutineScope.launch(dispatchers.io) { - sendPreProcessedMedia( - mediaUploadInfo = mediaUploadInfo, - caption = caption, - sendActionState = sendActionState, - dismissAfterSend = false, - inReplyToEventId = inReplyToEventId, - ) - - // Clean up the pre-processed media after it's been sent - mediaSender.cleanUp() + try { + sendPreProcessedMedia( + mediaUploadInfo = mediaUploadInfo, + caption = caption, + sendActionState = sendActionState, + dismissAfterSend = false, + inReplyToEventId = inReplyToEventId, + ) + } finally { + editedTempFileToDelete?.safeDelete() + // Clean up the pre-processed media after it's been sent + mediaSender.cleanUp() + } } } } AttachmentsPreviewEvent.CancelAndDismiss -> { displayFileTooLargeError = false + displayImageEditError = false + isApplyingImageEdits = false // Cancel media preprocessing and sending preprocessMediaJob?.cancel() + preprocessMediaJob = null // If we couldn't send the pre-processed media, remove it mediaSender.cleanUp() ongoingSendAttachmentJob.value?.cancel() // Dismiss the screen - dismiss( - attachment, - sendActionState, - ) + dismiss(sendActionState, editedTempFile) } AttachmentsPreviewEvent.CancelAndClearSendState -> { // Cancel media sending @@ -220,11 +267,100 @@ class AttachmentsPreviewPresenter( SendActionState.Idle } } + AttachmentsPreviewEvent.OpenImageEditor -> { + val resolvedCanEditImage = canEditImage || originalLocalMedia.info.canEditImage() + if (resolvedCanEditImage) { + preprocessMediaJob?.cancel() + preprocessMediaJob = null + resetPreparedMedia(sendActionState) + imageEditorState = AttachmentImageEditorState( + localMedia = originalLocalMedia, + edits = appliedImageEdits, + previewDebug = false, + ) + } + } + AttachmentsPreviewEvent.CloseImageEditor -> { + imageEditorState = null + } + is AttachmentsPreviewEvent.UpdateImageCropRect -> { + val pendingState = imageEditorState ?: return + imageEditorState = pendingState.copy( + edits = pendingState.edits.copy(cropRect = event.cropRect) + ) + } + AttachmentsPreviewEvent.RotateImageToTheLeft -> { + val pendingState = imageEditorState ?: return + imageEditorState = pendingState.copy( + edits = pendingState.edits.rotateAntiClockwise() + ) + } + AttachmentsPreviewEvent.FlipImageHorizontally -> { + val pendingState = imageEditorState ?: return + imageEditorState = pendingState.copy( + edits = pendingState.edits.flipHorizontally() + ) + } + AttachmentsPreviewEvent.FlipImageVertically -> { + val pendingState = imageEditorState ?: return + imageEditorState = pendingState.copy( + edits = pendingState.edits.flipVertically() + ) + } + AttachmentsPreviewEvent.ResetImageEdits -> { + imageEditorState = imageEditorState?.copy( + edits = AttachmentImageEdits() + ) + } + AttachmentsPreviewEvent.ApplyImageEdits -> { + val pendingState = imageEditorState ?: return + if (!pendingState.edits.hasChanges) { + editedTempFile?.safeDelete() + editedTempFile = null + appliedImageEdits = pendingState.edits + currentAttachment = Attachment.Media(originalLocalMedia) + imageEditorState = null + resetPreparedMedia(sendActionState) + return + } + isApplyingImageEdits = true + displayImageEditError = false + coroutineScope.launch { + val result = withContext(dispatchers.io) { + attachmentImageEditor.exportEdits( + localMedia = originalLocalMedia, + edits = pendingState.edits, + ) + } + result.fold( + onSuccess = { editedMedia -> + editedTempFile?.safeDelete() + editedTempFile = editedMedia.file + appliedImageEdits = pendingState.edits + currentAttachment = Attachment.Media(editedMedia.localMedia) + imageEditorState = null + resetPreparedMedia(sendActionState) + }, + onFailure = { + Timber.e(it, "Failed to apply image edits") + displayImageEditError = true + } + ) + isApplyingImageEdits = false + } + } + AttachmentsPreviewEvent.ClearImageEditError -> { + displayImageEditError = false + } } } return AttachmentsPreviewState( - attachment = attachment, + attachment = currentAttachment, + imageEditorState = imageEditorState, + canEditImage = canEditImage, + isApplyingImageEdits = isApplyingImageEdits, + displayImageEditError = displayImageEditError, sendActionState = sendActionState.value, textEditorState = textEditorState, mediaOptimizationSelectorState = mediaOptimizationSelectorState, @@ -233,6 +369,28 @@ class AttachmentsPreviewPresenter( ) } + private suspend fun getAutoPreprocessMediaOptimizationConfig( + mediaAttachment: Attachment.Media, + mediaOptimizationSelectorState: MediaOptimizationSelectorState, + ): MediaOptimizationConfig? { + return if (mediaAttachment.sendAsFile) { + // If we're sending the media as a file, we can skip image compression and we should select the highest video compression preset that still fits + // the upload limit (if the estimations are available) + val videoCompressionPreset = videoCompressionPresetSelector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.HIGH, + videoSizeEstimations = mediaOptimizationSelectorState.videoSizeEstimations, + ).dataOrNull() ?: VideoCompressionPreset.HIGH + + MediaOptimizationConfig( + compressImages = false, + videoCompressionPreset = videoCompressionPreset, + ) + } else { + // Otherwise, we just rely on the user preferences for media optimization + mediaOptimizationConfigProvider.get() + } + } + private fun CoroutineScope.preProcessAttachment( attachment: Attachment, mediaOptimizationConfig: MediaOptimizationConfig, @@ -279,8 +437,8 @@ class AttachmentsPreviewPresenter( } private fun dismiss( - attachment: Attachment, sendActionState: MutableState, + editedTempFile: File?, ) { // Delete the temporary file when (attachment) { @@ -291,6 +449,7 @@ class AttachmentsPreviewPresenter( } } } + editedTempFile?.safeDelete() // Reset the sendActionState to ensure that dialog is closed before the screen sendActionState.value = SendActionState.Done onDoneListener() @@ -304,6 +463,12 @@ class AttachmentsPreviewPresenter( } } + private fun resetPreparedMedia(sendActionState: MutableState) { + sendActionState.value.mediaUploadInfo()?.let(::cleanUp) + mediaSender.cleanUp() + sendActionState.value = SendActionState.Idle + } + private suspend fun sendPreProcessedMedia( mediaUploadInfo: MediaUploadInfo, caption: String?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index 97ca230d77..463479fe55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -10,12 +10,17 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEditorState import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.textcomposer.model.TextEditorState data class AttachmentsPreviewState( val attachment: Attachment, + val imageEditorState: AttachmentImageEditorState?, + val canEditImage: Boolean, + val isApplyingImageEdits: Boolean, + val displayImageEditError: Boolean, val sendActionState: SendActionState, val textEditorState: TextEditorState, val mediaOptimizationSelectorState: MediaOptimizationSelectorState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 70d7ab006e..a2df440a12 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -11,6 +11,8 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEditorState +import io.element.android.features.messages.impl.attachments.preview.imageeditor.anAttachmentImageEditorState import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState import io.element.android.features.messages.impl.attachments.video.VideoUploadEstimation import io.element.android.libraries.architecture.AsyncData @@ -42,6 +44,9 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider false + is SendActionState.Sending.Processing -> !state.sendActionState.displayProgress + SendActionState.Done -> false + else -> true + } + fun postSendAttachment() { state.eventSink(AttachmentsPreviewEvent.SendAttachment) } @@ -93,33 +107,90 @@ fun AttachmentsPreviewView( state.eventSink(AttachmentsPreviewEvent.CancelAndClearSendState) } - BackHandler(enabled = state.sendActionState !is SendActionState.Sending.Uploading && state.sendActionState !is SendActionState.Done) { - postCancel() + fun postOpenImageEditor() { + state.eventSink(AttachmentsPreviewEvent.OpenImageEditor) } - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - navigationIcon = { - BackButton( - imageVector = CompoundIcons.Close(), - onClick = ::postCancel, - ) - }, - title = {}, + fun postCloseImageEditor() { + state.eventSink(AttachmentsPreviewEvent.CloseImageEditor) + } + + fun postResetImageEditor() { + state.eventSink(AttachmentsPreviewEvent.ResetImageEdits) + } + + fun postApplyImageEdits() { + state.eventSink(AttachmentsPreviewEvent.ApplyImageEdits) + } + + BackHandler(enabled = state.sendActionState !is SendActionState.Sending.Uploading && state.sendActionState !is SendActionState.Done) { + if (state.imageEditorState != null) { + postCloseImageEditor() + } else { + postCancel() + } + } + + if (state.imageEditorState != null) { + AttachmentImageEditorView( + state = state.imageEditorState, + onCropRectChange = { cropRect -> + state.eventSink(AttachmentsPreviewEvent.UpdateImageCropRect(cropRect)) + }, + onRotateClick = { state.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) }, + onFlipHorizontallyClick = { state.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally) }, + onFlipVerticallyClick = { state.eventSink(AttachmentsPreviewEvent.FlipImageVertically) }, + onCancelClick = ::postCloseImageEditor, + onResetClick = ::postResetImageEditor, + onDoneClick = ::postApplyImageEdits, + modifier = modifier, + ) + } else { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + BackButton( + onClick = ::postCancel, + ) + }, + title = { + Text( + modifier = Modifier.semantics { + heading() + }, + text = stringResource(R.string.screen_media_upload_preview_title), + ) + }, + actions = { + if (state.canEditImage && canShowEditAction) { + IconButton( + onClick = ::postOpenImageEditor, + ) { + Icon( + imageVector = CompoundIcons.Crop(), + contentDescription = stringResource(CommonStrings.action_edit), + ) + } + } + } + ) + } + ) { paddingValues -> + AttachmentPreviewContent( + modifier = Modifier.padding(paddingValues), + state = state, + localMediaRenderer = localMediaRenderer, + onSendClick = ::postSendAttachment, ) } - ) { paddingValues -> - AttachmentPreviewContent( - modifier = Modifier.padding(paddingValues), - state = state, - localMediaRenderer = localMediaRenderer, - onSendClick = ::postSendAttachment, - ) } AttachmentSendStateView( sendActionState = state.sendActionState, + isApplyingImageEdits = state.isApplyingImageEdits, + displayImageEditError = state.displayImageEditError, + onDismissImageEditError = { state.eventSink(AttachmentsPreviewEvent.ClearImageEditError) }, onDismissClick = ::postClearSendState, onRetryClick = ::postSendAttachment ) @@ -128,36 +199,56 @@ fun AttachmentsPreviewView( @Composable private fun AttachmentSendStateView( sendActionState: SendActionState, + isApplyingImageEdits: Boolean, + displayImageEditError: Boolean, + onDismissImageEditError: () -> Unit, onDismissClick: () -> Unit, onRetryClick: () -> Unit ) { - when (sendActionState) { - is SendActionState.Sending.Processing -> { - if (sendActionState.displayProgress) { + when { + isApplyingImageEdits -> { + ProgressDialog( + type = ProgressDialogType.Indeterminate, + text = stringResource(CommonStrings.common_preparing), + showCancelButton = false, + onDismissRequest = {}, + ) + } + displayImageEditError -> { + AlertDialog( + title = stringResource(CommonStrings.common_error), + content = stringResource(CommonStrings.common_something_went_wrong_message), + onDismiss = onDismissImageEditError, + ) + } + else -> when (sendActionState) { + is SendActionState.Sending.Processing -> { + if (sendActionState.displayProgress) { + ProgressDialog( + type = ProgressDialogType.Indeterminate, + text = stringResource(CommonStrings.common_preparing), + showCancelButton = true, + onDismissRequest = onDismissClick, + ) + } + } + is SendActionState.Sending.Uploading -> { ProgressDialog( type = ProgressDialogType.Indeterminate, - text = stringResource(CommonStrings.common_preparing), + text = stringResource(id = CommonStrings.common_sending), showCancelButton = true, onDismissRequest = onDismissClick, ) } + is SendActionState.Failure -> { + RetryDialog( + content = stringResource(sendAttachmentError(sendActionState.error)), + onDismiss = onDismissClick, + onRetry = onRetryClick + ) + } + else -> Unit } - is SendActionState.Sending.Uploading -> { - ProgressDialog( - type = ProgressDialogType.Indeterminate, - text = stringResource(id = CommonStrings.common_sending), - showCancelButton = true, - onDismissRequest = onDismissClick, - ) - } - is SendActionState.Failure -> { - RetryDialog( - content = stringResource(sendAttachmentError(sendActionState.error)), - onDismiss = onDismissClick, - onRetry = onRetryClick - ) - } - else -> Unit } } @@ -184,10 +275,10 @@ private fun AttachmentPreviewContent( } } } - val mimeType = (state.attachment as? Attachment.Media)?.localMedia?.info?.mimeType - if (mimeType?.isMimeTypeImage() == true) { + val mediaInfo = (state.attachment as? Attachment.Media)?.localMedia?.info + if (mediaInfo?.isImageAttachment() == true) { ImageOptimizationSelector(state.mediaOptimizationSelectorState) - } else if (mimeType?.isMimeTypeVideo() == true) { + } else if (mediaInfo?.mimeType?.isMimeTypeVideo() == true) { VideoPresetSelector(state = state.mediaOptimizationSelectorState) } @@ -220,7 +311,8 @@ private fun AttachmentPreviewContent( private fun ImageOptimizationSelector(state: MediaOptimizationSelectorState) { if (state.displayMediaSelectorViews == true) { Row( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .niceClickable { state.isImageOptimizationEnabled?.let { value -> state.eventSink(MediaOptimizationSelectorEvent.SelectImageOptimization(!value)) @@ -229,7 +321,9 @@ private fun ImageOptimizationSelector(state: MediaOptimizationSelectorState) { .padding(horizontal = 16.dp, vertical = 16.dp) ) { Text( - modifier = Modifier.weight(1f).align(Alignment.CenterVertically), + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically), text = stringResource(R.string.screen_media_upload_preview_optimize_image_quality_title), style = ElementTheme.typography.fontBodyLgRegular, ) @@ -255,7 +349,8 @@ private fun VideoPresetSelector( if (state.displayMediaSelectorViews == true && videoPresets != null && state.selectedVideoPreset != null) { Column( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp) .niceClickable { state.eventSink(MediaOptimizationSelectorEvent.OpenVideoPresetSelectorDialog) } ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditor.kt new file mode 100644 index 0000000000..f8d199a74e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditor.kt @@ -0,0 +1,202 @@ +/* + * 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.attachments.preview.imageeditor + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import androidx.exifinterface.media.ExifInterface +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.messages.impl.attachments.preview.resolvedImageMimeType +import io.element.android.libraries.androidutils.bitmap.rotateToExifMetadataOrientation +import io.element.android.libraries.androidutils.bitmap.writeBitmap +import io.element.android.libraries.androidutils.file.createTmpFile +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAnimatedImage +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import kotlinx.coroutines.withContext +import java.io.File +import kotlin.math.roundToInt + +private const val EDITED_MEDIA_DIR_NAME = "edited-media" + +interface AttachmentImageEditor { + suspend fun canEdit(localMedia: LocalMedia): Boolean + + suspend fun exportEdits( + localMedia: LocalMedia, + edits: AttachmentImageEdits, + ): Result +} + +data class EditedLocalMedia( + val localMedia: LocalMedia, + val file: File, +) + +@ContributesBinding(AppScope::class) +class DefaultAttachmentImageEditor( + @ApplicationContext private val context: Context, + private val dispatchers: CoroutineDispatchers, +) : AttachmentImageEditor { + override suspend fun canEdit(localMedia: LocalMedia): Boolean = withContext(dispatchers.io) { + localMedia.info.resolvedImageMimeType() + ?.takeIf { it.isEditableStillImageMimeType() } + ?.let { return@withContext true } + + val decodedMimeType = context.contentResolver.openInputStream(localMedia.uri)?.use { input -> + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeStream(input, null, options) + options.outMimeType + } + + decodedMimeType.isEditableStillImageMimeType() + } + + override suspend fun exportEdits( + localMedia: LocalMedia, + edits: AttachmentImageEdits, + ): Result = withContext(dispatchers.io) { + runCatchingExceptions { + val sourceMimeType = localMedia.info.resolvedImageMimeType() ?: localMedia.info.mimeType + val exportedMimeType = exportedMimeTypeFor(sourceMimeType) + val exifOrientation = context.contentResolver.openInputStream(localMedia.uri)?.let { input -> + input.use { + ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } + } ?: ExifInterface.ORIENTATION_UNDEFINED + + val decodedBitmap = context.contentResolver.openInputStream(localMedia.uri)?.use { input -> + BitmapFactory.decodeStream(input) + } ?: error("Unable to decode image from ${localMedia.uri}") + + val normalizedBitmap = decodedBitmap.rotateToExifMetadataOrientation(exifOrientation) + if (normalizedBitmap !== decodedBitmap) { + decodedBitmap.recycle() + } + + val transformedBitmap = normalizedBitmap.applyEdits(edits) + if (transformedBitmap !== normalizedBitmap) { + normalizedBitmap.recycle() + } + + val cropRect = edits.cropRect.toPixelRect( + imageWidth = transformedBitmap.width, + imageHeight = transformedBitmap.height, + ) + val isCropUnchanged = cropRect.left == 0 && cropRect.top == 0 && + cropRect.width() == transformedBitmap.width && cropRect.height() == transformedBitmap.height + val croppedBitmap = if (isCropUnchanged) { + transformedBitmap + } else { + Bitmap.createBitmap( + transformedBitmap, + cropRect.left, + cropRect.top, + cropRect.width(), + cropRect.height(), + ) + } + if (croppedBitmap !== transformedBitmap) { + transformedBitmap.recycle() + } + + val editedMediaDir = File(context.cacheDir, EDITED_MEDIA_DIR_NAME).apply { mkdirs() } + val outputFile = context.createTmpFile(baseDir = editedMediaDir, extension = compressFileExtension(exportedMimeType)) + outputFile.writeBitmap( + bitmap = croppedBitmap, + format = compressFormat(exportedMimeType), + quality = 90, + ) + croppedBitmap.recycle() + + EditedLocalMedia( + localMedia = localMedia.copy( + uri = Uri.fromFile(outputFile), + info = localMedia.info.copy(mimeType = exportedMimeType), + ), + file = outputFile, + ) + } + } +} + +internal fun exportedMimeTypeFor(sourceMimeType: String?): String { + return if (sourceMimeType == MimeTypes.Png) { + MimeTypes.Png + } else { + MimeTypes.Jpeg + } +} + +private fun Bitmap.applyEdits(edits: AttachmentImageEdits): Bitmap { + val normalizedTurns = (edits.rotationQuarterTurns % 4 + 4) % 4 + if (normalizedTurns == 0 && !edits.isFlippedHorizontally && !edits.isFlippedVertically) { + return this + } + val centerX = width / 2f + val centerY = height / 2f + val matrix = Matrix().apply { + val scaleX = if (edits.isFlippedHorizontally) -1f else 1f + val scaleY = if (edits.isFlippedVertically) -1f else 1f + if (scaleX < 0f || scaleY < 0f) { + postScale(scaleX, scaleY, centerX, centerY) + } + if (normalizedTurns != 0) { + postRotate(normalizedTurns * 90f, centerX, centerY) + } + } + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) +} + +private data class PixelCropRect( + val left: Int, + val top: Int, + val right: Int, + val bottom: Int, +) { + fun width() = right - left + fun height() = bottom - top +} + +private fun NormalizedCropRect.toPixelRect(imageWidth: Int, imageHeight: Int): PixelCropRect { + val leftPx = (left * imageWidth).roundToInt().coerceIn(0, imageWidth - 1) + val topPx = (top * imageHeight).roundToInt().coerceIn(0, imageHeight - 1) + val rightPx = (right * imageWidth).roundToInt().coerceIn(leftPx + 1, imageWidth) + val bottomPx = (bottom * imageHeight).roundToInt().coerceIn(topPx + 1, imageHeight) + return PixelCropRect( + left = leftPx, + top = topPx, + right = rightPx, + bottom = bottomPx, + ) +} + +private fun compressFormat(mimeType: String) = when (mimeType) { + MimeTypes.Png -> Bitmap.CompressFormat.PNG + else -> Bitmap.CompressFormat.JPEG +} + +private fun compressFileExtension(mimeType: String) = when (mimeType) { + MimeTypes.Png -> "png" + else -> "jpeg" +} + +private fun String?.isEditableStillImageMimeType(): Boolean { + return this != null && + this.isMimeTypeImage() && + !this.isMimeTypeAnimatedImage() && + this != MimeTypes.Svg +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt new file mode 100644 index 0000000000..b798df2747 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorState.kt @@ -0,0 +1,197 @@ +/* + * 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.attachments.preview.imageeditor + +import androidx.annotation.FloatRange +import androidx.compose.runtime.Immutable +import io.element.android.libraries.mediaviewer.api.local.LocalMedia + +private const val DEFAULT_CROP_MARGIN = 0f +private const val MIN_CROP_SIZE = 0.1f + +@Immutable +data class AttachmentImageEditorState( + val localMedia: LocalMedia, + val edits: AttachmentImageEdits, + // For preview only + val previewDebug: Boolean, +) + +@Immutable +data class AttachmentImageEdits( + val cropRect: NormalizedCropRect = NormalizedCropRect.default(), + val rotationQuarterTurns: Int = 0, + val isFlippedHorizontally: Boolean = false, + val isFlippedVertically: Boolean = false, +) { + val normalizedRotationQuarterTurns: Int + get() = rotationQuarterTurns % 4 + + val rotationDegrees: Int + get() = normalizedRotationQuarterTurns * 90 + + val hasChanges: Boolean + get() = cropRect != NormalizedCropRect.default() || + normalizedRotationQuarterTurns != 0 || + isFlippedHorizontally || + isFlippedVertically + + fun rotateAntiClockwise(): AttachmentImageEdits { + return copy( + rotationQuarterTurns = (normalizedRotationQuarterTurns + 3) % 4, + // Also update the crop rect to keep the same selected area + cropRect = cropRect.rotateAntiClockwise() + ) + } + + fun flipHorizontally(): AttachmentImageEdits { + return copy( + isFlippedHorizontally = !isFlippedHorizontally, + // Also update the crop rect to keep the same selected area + cropRect = cropRect.flipHorizontally(), + ) + } + + fun flipVertically(): AttachmentImageEdits { + return copy( + isFlippedVertically = !isFlippedVertically, + // Also update the crop rect to keep the same selected area + cropRect = cropRect.flipVertically(), + ) + } +} + +@Immutable +data class NormalizedCropRect( + @FloatRange(from = 0.0, to = 1.0) val left: Float, + @FloatRange(from = 0.0, to = 1.0) val top: Float, + @FloatRange(from = 0.0, to = 1.0) val right: Float, + @FloatRange(from = 0.0, to = 1.0) val bottom: Float, +) { + init { + require(left in 0f..1f) + require(top in 0f..1f) + require(right in 0f..1f) + require(bottom in 0f..1f) + require(left < right) + require(top < bottom) + } + + val width: Float + get() = right - left + + val height: Float + get() = bottom - top + + fun applyChange( + dragTarget: CropDragTarget, + deltaX: Float, + deltaY: Float, + ): NormalizedCropRect = when (dragTarget) { + is CropDragTarget.Move -> translate(deltaX, deltaY) + is CropDragTarget.Corner -> dragWithCorner(dragTarget, deltaX, deltaY) + is CropDragTarget.Edge -> dragWithEdge(dragTarget, deltaX, deltaY) + } + + private fun translate(deltaX: Float, deltaY: Float): NormalizedCropRect { + val clampedLeft = (left + deltaX).coerceIn(0f, 1f - width) + val clampedTop = (top + deltaY).coerceIn(0f, 1f - height) + return copy( + left = clampedLeft, + top = clampedTop, + right = clampedLeft + width, + bottom = clampedTop + height, + ) + } + + private fun dragWithCorner( + dragTarget: CropDragTarget.Corner, + deltaX: Float, + deltaY: Float, + ) = when (dragTarget) { + CropDragTarget.Corner.TopLeft -> copy( + left = (left + deltaX).coerceIn(0f, right - MIN_CROP_SIZE), + top = (top + deltaY).coerceIn(0f, bottom - MIN_CROP_SIZE), + ) + CropDragTarget.Corner.TopRight -> copy( + right = (right + deltaX).coerceIn(left + MIN_CROP_SIZE, 1f), + top = (top + deltaY).coerceIn(0f, bottom - MIN_CROP_SIZE), + ) + CropDragTarget.Corner.BottomRight -> copy( + right = (right + deltaX).coerceIn(left + MIN_CROP_SIZE, 1f), + bottom = (bottom + deltaY).coerceIn(top + MIN_CROP_SIZE, 1f), + ) + CropDragTarget.Corner.BottomLeft -> copy( + left = (left + deltaX).coerceIn(0f, right - MIN_CROP_SIZE), + bottom = (bottom + deltaY).coerceIn(top + MIN_CROP_SIZE, 1f), + ) + } + + private fun dragWithEdge( + dragTarget: CropDragTarget.Edge, + deltaX: Float, + deltaY: Float, + ) = when (dragTarget) { + CropDragTarget.Edge.Top -> copy( + top = (top + deltaY).coerceIn(0f, bottom - MIN_CROP_SIZE), + ) + CropDragTarget.Edge.Right -> copy( + right = (right + deltaX).coerceIn(left + MIN_CROP_SIZE, 1f), + ) + CropDragTarget.Edge.Bottom -> copy( + bottom = (bottom + deltaY).coerceIn(top + MIN_CROP_SIZE, 1f), + ) + CropDragTarget.Edge.Left -> copy( + left = (left + deltaX).coerceIn(0f, right - MIN_CROP_SIZE), + ) + } + + fun rotateAntiClockwise() = copy( + left = top, + top = 1f - right, + right = bottom, + bottom = 1f - left, + ) + + fun flipHorizontally() = copy( + left = 1f - right, + right = 1f - left, + ) + + fun flipVertically() = copy( + top = 1f - bottom, + bottom = 1f - top, + ) + + companion object { + fun default() = NormalizedCropRect( + left = DEFAULT_CROP_MARGIN, + top = DEFAULT_CROP_MARGIN, + right = 1f - DEFAULT_CROP_MARGIN, + bottom = 1f - DEFAULT_CROP_MARGIN, + ) + } +} + +sealed interface CropDragTarget { + data object Move : CropDragTarget + + sealed interface Corner : CropDragTarget { + data object TopLeft : Corner + data object TopRight : Corner + data object BottomRight : Corner + data object BottomLeft : Corner + } + + sealed interface Edge : CropDragTarget { + data object Top : Edge + data object Right : Edge + data object Bottom : Edge + data object Left : Edge + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt new file mode 100644 index 0000000000..7c542fd1a8 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.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.messages.impl.attachments.preview.imageeditor + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.core.net.toUri +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo +import io.element.android.libraries.mediaviewer.api.local.LocalMedia + +open class AttachmentImageEditorStateProvider : PreviewParameterProvider { + private val caterpillarCrop = NormalizedCropRect( + left = 0.3f, + top = 0.3f, + right = 0.8f, + bottom = 0.75f, + ) + + override val values: Sequence + get() = sequenceOf( + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + // Cheat a bit so that the crop match the sample image size (1024 * 682) + cropRect = 0.17f.let { correction -> + NormalizedCropRect( + left = 0f, + top = correction, + right = 1f, + bottom = 1 - correction, + ) + }, + ), + ), + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = caterpillarCrop, + ), + ), + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = caterpillarCrop, + ), + previewDebug = true, + ), + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = caterpillarCrop, + ).rotateAntiClockwise(), + ), + // Small crop + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = NormalizedCropRect( + left = 0.3f, + top = 0.6f, + right = 0.4f, + bottom = 0.7f, + ), + ), + previewDebug = true, + ), + // Big crop + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = NormalizedCropRect( + left = 0.05f, + top = 0.05f, + right = 0.95f, + bottom = 0.95f, + ), + ), + previewDebug = true, + ), + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = caterpillarCrop, + ).flipHorizontally(), + ), + anAttachmentImageEditorState( + edits = AttachmentImageEdits( + cropRect = caterpillarCrop, + ).flipVertically(), + ), + ) +} + +internal fun anAttachmentImageEditorState( + localMedia: LocalMedia = LocalMedia( + uri = "file://preview-image".toUri(), + info = anImageMediaInfo(), + ), + edits: AttachmentImageEdits = AttachmentImageEdits(), + previewDebug: Boolean = false, +) = AttachmentImageEditorState( + localMedia = localMedia, + edits = edits, + previewDebug = previewDebug, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt new file mode 100644 index 0000000000..a5317b96c6 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt @@ -0,0 +1,691 @@ +/* + * 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.attachments.preview.imageeditor + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.compose.AsyncImagePainter +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.R +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.text.toPx +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.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.ui.strings.CommonStrings +import kotlin.math.min + +private val minHandleTouchRadius = 16.dp +private val maxHandleTouchRadius = 56.dp + +/** + * Ref: https://www.figma.com/design/zftpgS6LjiczobJZ1GUNpt/Updates-to-Media---File-Upload?node-id=51-3539 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AttachmentImageEditorView( + state: AttachmentImageEditorState, + onCropRectChange: (NormalizedCropRect) -> Unit, + onRotateClick: () -> Unit, + onFlipHorizontallyClick: () -> Unit, + onFlipVerticallyClick: () -> Unit, + onResetClick: () -> Unit, + onCancelClick: () -> Unit, + onDoneClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val rotateContentDescription = stringResource(R.string.screen_image_edition_a11y_rotate_to_the_left) + val rotationStateDescription = pluralStringResource( + R.plurals.screen_image_edition_a11y_rotation_state, + state.edits.rotationDegrees, + state.edits.rotationDegrees, + ) + val flipHorizontalLabel = stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally) + val flipHorizontalState = if (state.edits.isFlippedHorizontally) { + stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally_state_flipped) + } else { + stringResource(R.string.screen_image_edition_a11y_flip_image_horizontally_state_original) + } + val flipVerticalLabel = stringResource(R.string.screen_image_edition_a11y_flip_image_vertically) + val flipVerticalState = if (state.edits.isFlippedVertically) { + stringResource(R.string.screen_image_edition_a11y_flip_image_vertically_state_flipped) + } else { + stringResource(R.string.screen_image_edition_a11y_flip_image_vertically_state_original) + } + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + TopAppBar( + navigationIcon = { + BackButton( + imageVector = CompoundIcons.Close(), + onClick = onCancelClick, + ) + }, + title = { + Text( + modifier = Modifier.semantics { + heading() + }, + text = stringResource(R.string.screen_image_edition_title), + ) + }, + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(ElementTheme.colors.bgCanvasDefault) + .padding(paddingValues) + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + CropEditorCanvas( + state = state, + onCropRectChange = onCropRectChange, + ) + } + Row( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .widthIn(max = 360.dp) + .navigationBarsPadding() + .padding(start = 20.dp, top = 18.dp, end = 20.dp, bottom = 18.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterStart, + ) { + TextButton( + text = stringResource(CommonStrings.action_reset), + destructive = true, + onClick = onResetClick, + ) + } + Row( + modifier = Modifier.weight(2f), + // Center the content horizontally + horizontalArrangement = Arrangement.Center, + ) { + IconButton( + onClick = onFlipHorizontallyClick, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = flipHorizontalLabel + stateDescription = flipHorizontalState + } + ) { + Icon( + imageVector = CompoundIcons.FlipHorizontal(), + contentDescription = null, + ) + } + IconButton( + onClick = onRotateClick, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = rotateContentDescription + stateDescription = rotationStateDescription + } + ) { + Icon( + imageVector = CompoundIcons.RotateLeft(), + contentDescription = null, + ) + } + IconButton( + onClick = onFlipVerticallyClick, + modifier = Modifier + .clearAndSetSemantics { + contentDescription = flipVerticalLabel + stateDescription = flipVerticalState + } + ) { + Icon( + imageVector = CompoundIcons.FlipVertical(), + contentDescription = null, + ) + } + } + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterEnd, + ) { + TextButton( + text = stringResource(CommonStrings.action_save), + onClick = onDoneClick, + ) + } + } + } + } +} + +@Composable +private fun BoxScope.CropEditorCanvas( + state: AttachmentImageEditorState, + onCropRectChange: (NormalizedCropRect) -> Unit, +) { + var imageSize by remember(state.localMedia.uri) { mutableStateOf(IntSize.Zero) } + val rotationQuarterTurns = state.edits.normalizedRotationQuarterTurns + val flipScaleX = if (state.edits.isFlippedHorizontally) -1f else 1f + val flipScaleY = if (state.edits.isFlippedVertically) -1f else 1f + + var imageRect by remember { mutableStateOf(Rect.Zero) } + + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + ) { + val displayedSize = remember(maxWidth, maxHeight, imageSize, rotationQuarterTurns) { + val sourceWidth = imageSize.width.takeIf { it > 0 } ?: 1 + val sourceHeight = imageSize.height.takeIf { it > 0 } ?: 1 + val aspectRatio = if (rotationQuarterTurns % 2 == 0) { + sourceWidth.toFloat() / sourceHeight.toFloat() + } else { + sourceHeight.toFloat() / sourceWidth.toFloat() + } + fitSize( + containerWidth = constraints.maxWidth.toFloat(), + containerHeight = constraints.maxHeight.toFloat(), + aspectRatio = aspectRatio, + ) + } + val density = LocalDensity.current + val displayedWidthDp = with(density) { displayedSize.width.toDp() } + val displayedHeightDp = with(density) { displayedSize.height.toDp() } + val imageLayoutSize = remember(displayedSize, rotationQuarterTurns) { + if (rotationQuarterTurns % 2 == 0) { + displayedSize + } else { + Size( + width = displayedSize.height, + height = displayedSize.width, + ) + } + } + val imageLayoutWidthDp = with(density) { imageLayoutSize.width.toDp() } + val imageLayoutHeightDp = with(density) { imageLayoutSize.height.toDp() } + + Box( + modifier = Modifier + .size(displayedWidthDp, displayedHeightDp) + .align(Alignment.Center) + .onPlaced { + imageRect = it.boundsInParent() + }, + contentAlignment = Alignment.Center, + ) { + if (LocalInspectionMode.current) { + Image( + painter = painterResource(id = CommonDrawables.sample_background), + contentDescription = null, + modifier = Modifier + .requiredSize(imageLayoutWidthDp, imageLayoutHeightDp) + .graphicsLayer { + scaleX = flipScaleX + scaleY = flipScaleY + } + .graphicsLayer { rotationZ = rotationQuarterTurns * 90f }, + contentScale = ContentScale.Fit, + ) + } else { + AsyncImage( + model = state.localMedia.uri, + contentDescription = stringResource(CommonStrings.common_image), + modifier = Modifier + .requiredSize(imageLayoutWidthDp, imageLayoutHeightDp) + .graphicsLayer { + scaleX = flipScaleX + scaleY = flipScaleY + } + .graphicsLayer { rotationZ = rotationQuarterTurns * 90f }, + contentScale = ContentScale.Fit, + onState = { painterState -> + if (painterState is AsyncImagePainter.State.Success) { + imageSize = IntSize( + width = painterState.result.image.width, + height = painterState.result.image.height, + ) + } + } + ) + } + } + val minHandleTouchRadiusPx = minHandleTouchRadius.toPx() + val maxHandleTouchRadiusPx = maxHandleTouchRadius.toPx() + val touchRadiusPx by rememberUpdatedState( + (min( + state.edits.cropRect.width * imageRect.width, + state.edits.cropRect.height * imageRect.height, + ) / 4f).coerceIn( + minHandleTouchRadiusPx, + maxHandleTouchRadiusPx, + ) + ) + var dragTarget by remember { mutableStateOf(null) } + val latestCropRect by rememberUpdatedState(state.edits.cropRect) + val drawGuidelines = dragTarget == CropDragTarget.Move || state.previewDebug + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { offset -> + dragTarget = detectDragTarget( + touchPoint = offset, + imageOffset = imageRect.topLeft, + cropRect = latestCropRect, + canvasSize = Size(imageRect.width, imageRect.height), + handleTouchRadius = touchRadiusPx, + ) + }, + onDragCancel = { + dragTarget = null + }, + onDragEnd = { + dragTarget = null + }, + ) { change, dragAmount -> + val activeTarget = dragTarget ?: return@detectDragGestures + change.consume() + val gestureAreaWidth = imageRect.width.takeIf { it > 0f } ?: size.width.toFloat() + val gestureAreaHeight = imageRect.height.takeIf { it > 0f } ?: size.height.toFloat() + onCropRectChange( + latestCropRect.applyChange( + dragTarget = activeTarget, + deltaX = dragAmount.x / gestureAreaWidth, + deltaY = dragAmount.y / gestureAreaHeight, + ) + ) + } + }, + contentAlignment = Alignment.Center, + ) { + CropOverlay( + imageSize = DpSize(displayedWidthDp, displayedHeightDp), + cropRect = state.edits.cropRect, + drawGuidelines = drawGuidelines, + previewDebug = state.previewDebug, + touchRadiusPx = touchRadiusPx, + dragTarget = dragTarget, + ) + } + } +} + +@Composable +private fun CropOverlay( + imageSize: DpSize, + cropRect: NormalizedCropRect, + drawGuidelines: Boolean, + previewDebug: Boolean, + touchRadiusPx: Float, + dragTarget: CropDragTarget?, +) { + val borderColor = ElementTheme.colors.iconPrimary + val guideColor = ElementTheme.colors.iconPrimary + + Canvas( + modifier = Modifier.size(imageSize.width, imageSize.height) + ) { + val cropLeft = cropRect.left * size.width + val cropTop = cropRect.top * size.height + val cropRight = cropRect.right * size.width + val cropBottom = cropRect.bottom * size.height + // Hardcoded black: the crop overlay must always darken the image regardless of theme. + // No semantic token exists for this use case in the Compound design system. + val overlayColor = Color.Black.copy(alpha = 0.48f) + // Overlay above the crop area + drawRect( + color = overlayColor, + topLeft = Offset.Zero, + size = Size(width = size.width, height = cropTop), + ) + // Overlay on the left of the crop area + drawRect( + color = overlayColor, + topLeft = Offset(0f, cropTop), + size = Size(width = cropLeft, height = cropBottom - cropTop), + ) + // Overlay on the right of the crop area + drawRect( + color = overlayColor, + topLeft = Offset(cropRight, cropTop), + size = Size(width = size.width - cropRight, height = cropBottom - cropTop), + ) + // Overlay below the crop area + drawRect( + color = overlayColor, + topLeft = Offset(0f, cropBottom), + size = Size(width = size.width, height = size.height - cropBottom), + ) + // Main frame of the crop area + drawRect( + color = borderColor, + topLeft = Offset(cropLeft, cropTop), + size = Size(width = cropRight - cropLeft, height = cropBottom - cropTop), + style = Stroke(width = 1.dp.toPx()), + ) + // Guidelines dividing the crop area into 9 equal parts + if (drawGuidelines) { + val thirdWidth = (cropRight - cropLeft) / 3f + val thirdHeight = (cropBottom - cropTop) / 3f + for (index in 1..2) { + val offsetX = cropLeft + thirdWidth * index + val offsetY = cropTop + thirdHeight * index + // Vertical guide line + drawLine( + color = guideColor, + start = Offset(offsetX, cropTop), + end = Offset(offsetX, cropBottom), + strokeWidth = 1.dp.toPx(), + ) + // Horizontal guide line + drawLine( + color = guideColor, + start = Offset(cropLeft, offsetY), + end = Offset(cropRight, offsetY), + strokeWidth = 1.dp.toPx(), + ) + } + } + // Corner handles + val handleLength = 18.dp.toPx() + val handleOffset = 2.dp.toPx() + // Top left corner + drawCornerHandle( + x = cropLeft - handleOffset, + y = cropTop - handleOffset, + handleLength = handleLength, + color = borderColor, + position = CropDragTarget.Corner.TopLeft, + ) + // Top right corner + drawCornerHandle( + x = cropRight + handleOffset, + y = cropTop - handleOffset, + handleLength = handleLength, + color = borderColor, + position = CropDragTarget.Corner.TopRight, + ) + // Bottom left corner + drawCornerHandle( + x = cropLeft - handleOffset, + y = cropBottom + handleOffset, + handleLength = handleLength, + color = borderColor, + position = CropDragTarget.Corner.BottomLeft, + ) + // Bottom right corner + drawCornerHandle( + x = cropRight + handleOffset, + y = cropBottom + handleOffset, + handleLength = handleLength, + color = borderColor, + position = CropDragTarget.Corner.BottomRight, + ) + val handleColor = borderColor + // Top handle + drawEdgeHandle( + center = Offset((cropLeft + cropRight) / 2f, cropTop - handleOffset), + horizontal = true, + handleLength = handleLength, + color = handleColor, + ) + // Right handle + drawEdgeHandle( + center = Offset(cropRight + handleOffset, (cropTop + cropBottom) / 2f), + horizontal = false, + handleLength = handleLength, + color = handleColor, + ) + // Bottom handle + drawEdgeHandle( + center = Offset((cropLeft + cropRight) / 2f, cropBottom + handleOffset), + horizontal = true, + handleLength = handleLength, + color = handleColor, + ) + // Left handle + drawEdgeHandle( + center = Offset(cropLeft - handleOffset, (cropTop + cropBottom) / 2f), + horizontal = false, + handleLength = handleLength, + color = handleColor, + ) + + if (previewDebug) { + // Draw disk around touchable area + listOf( + CropDragTarget.Edge.Top, + CropDragTarget.Edge.Right, + CropDragTarget.Edge.Bottom, + CropDragTarget.Edge.Left, + CropDragTarget.Corner.TopLeft, + CropDragTarget.Corner.TopRight, + CropDragTarget.Corner.BottomRight, + CropDragTarget.Corner.BottomLeft, + CropDragTarget.Move, + ).forEach { target -> + val color = when (target) { + is CropDragTarget.Move -> Color.Red + is CropDragTarget.Corner -> Color.Blue + is CropDragTarget.Edge -> Color.Green + }.copy(alpha = if (dragTarget == target) 9f else 0.5f) + drawCircle( + color = color, + radius = touchRadiusPx, + center = computeOffset(target, cropRect, Size(size.width, size.height)), + ) + } + } + } +} + +private fun fitSize( + containerWidth: Float, + containerHeight: Float, + aspectRatio: Float, +): Size { + val widthBasedHeight = containerWidth / aspectRatio + return if (widthBasedHeight <= containerHeight) { + Size(width = containerWidth, height = widthBasedHeight) + } else { + Size(width = containerHeight * aspectRatio, height = containerHeight) + } +} + +private fun detectDragTarget( + touchPoint: Offset, + imageOffset: Offset, + cropRect: NormalizedCropRect, + canvasSize: Size, + handleTouchRadius: Float, +): CropDragTarget? { + // Give priority on Move (extra detection of the center of crop area) + // to ensure that user can move a small crop, then to corners and at last to edges. + val handlesArea = mapOf( + CropDragTarget.Move to computeOffset(CropDragTarget.Move, cropRect, canvasSize), + CropDragTarget.Corner.TopLeft to computeOffset(CropDragTarget.Corner.TopLeft, cropRect, canvasSize), + CropDragTarget.Corner.TopRight to computeOffset(CropDragTarget.Corner.TopRight, cropRect, canvasSize), + CropDragTarget.Corner.BottomRight to computeOffset(CropDragTarget.Corner.BottomRight, cropRect, canvasSize), + CropDragTarget.Corner.BottomLeft to computeOffset(CropDragTarget.Corner.BottomLeft, cropRect, canvasSize), + CropDragTarget.Edge.Top to computeOffset(CropDragTarget.Edge.Top, cropRect, canvasSize), + CropDragTarget.Edge.Right to computeOffset(CropDragTarget.Edge.Right, cropRect, canvasSize), + CropDragTarget.Edge.Bottom to computeOffset(CropDragTarget.Edge.Bottom, cropRect, canvasSize), + CropDragTarget.Edge.Left to computeOffset(CropDragTarget.Edge.Left, cropRect, canvasSize), + ) + handlesArea.forEach { (target, corner) -> + if ((corner - touchPoint + imageOffset).getDistance() <= handleTouchRadius) { + return target + } + } + val cropLeft = imageOffset.x + cropRect.left * canvasSize.width + val cropTop = imageOffset.y + cropRect.top * canvasSize.height + val cropRight = imageOffset.x + cropRect.right * canvasSize.width + val cropBottom = imageOffset.y + cropRect.bottom * canvasSize.height + return if (touchPoint.x in cropLeft..cropRight && touchPoint.y in cropTop..cropBottom) { + CropDragTarget.Move + } else { + null + } +} + +private fun computeOffset( + target: CropDragTarget, + cropRect: NormalizedCropRect, + canvasSize: Size, +) = when (target) { + CropDragTarget.Move -> Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f) + CropDragTarget.Corner.TopLeft -> Offset(cropRect.left * canvasSize.width, cropRect.top * canvasSize.height) + CropDragTarget.Edge.Top -> Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, cropRect.top * canvasSize.height) + CropDragTarget.Corner.TopRight -> Offset(cropRect.right * canvasSize.width, cropRect.top * canvasSize.height) + CropDragTarget.Edge.Right -> Offset(cropRect.right * canvasSize.width, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f) + CropDragTarget.Corner.BottomRight -> Offset(cropRect.right * canvasSize.width, cropRect.bottom * canvasSize.height) + CropDragTarget.Edge.Bottom -> Offset((cropRect.left + cropRect.right) * canvasSize.width / 2f, cropRect.bottom * canvasSize.height) + CropDragTarget.Corner.BottomLeft -> Offset(cropRect.left * canvasSize.width, cropRect.bottom * canvasSize.height) + CropDragTarget.Edge.Left -> Offset(cropRect.left * canvasSize.width, (cropRect.top + cropRect.bottom) * canvasSize.height / 2f) +} + +// x and y are the coordinates of the corner +private fun DrawScope.drawCornerHandle( + x: Float, + y: Float, + handleLength: Float, + color: Color, + position: CropDragTarget.Corner, +) { + val strokeWidth = 4.dp.toPx() + val correction = strokeWidth / 2 + val horizontalCorrection = if (position.isLeft()) -correction else correction + val horizontalEndX = if (position.isLeft()) x + handleLength else x - handleLength + val verticalEndY = if (position.isTop()) y + handleLength else y - handleLength + val verticalCorrection = if (position.isTop()) -correction else correction + // Horizontal line + drawLine( + color = color, + start = Offset(x + horizontalCorrection, y), + end = Offset(horizontalEndX + horizontalCorrection, y), + strokeWidth = strokeWidth, + ) + // Vertical line + drawLine( + color = color, + start = Offset(x, y + verticalCorrection), + end = Offset(x, verticalEndY + verticalCorrection), + strokeWidth = strokeWidth, + ) +} + +private fun CropDragTarget.Corner.isLeft() = this == CropDragTarget.Corner.TopLeft || this == CropDragTarget.Corner.BottomLeft +private fun CropDragTarget.Corner.isTop() = this == CropDragTarget.Corner.TopLeft || this == CropDragTarget.Corner.TopRight + +private fun DrawScope.drawEdgeHandle( + center: Offset, + horizontal: Boolean, + handleLength: Float, + color: Color, +) { + val start = if (horizontal) { + Offset(center.x - handleLength / 2f, center.y) + } else { + Offset(center.x, center.y - handleLength / 2f) + } + val end = if (horizontal) { + Offset(center.x + handleLength / 2f, center.y) + } else { + Offset(center.x, center.y + handleLength / 2f) + } + drawLine( + color = color, + start = start, + end = end, + strokeWidth = 4.dp.toPx(), + ) +} + +// Only preview in dark, dark theme is forced on the Node. +@Preview +@Composable +internal fun AttachmentImageEditorViewPreview( + @PreviewParameter(AttachmentImageEditorStateProvider::class) state: AttachmentImageEditorState, +) = ElementPreviewDark { + AttachmentImageEditorView( + state = state, + onCropRectChange = {}, + onRotateClick = {}, + onFlipHorizontallyClick = {}, + onFlipVerticallyClick = {}, + onResetClick = {}, + onCancelClick = {}, + onDoneClick = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt index c81c306f90..abc0264b2f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt @@ -37,9 +37,11 @@ import kotlin.math.roundToLong @AssistedInject class DefaultMediaOptimizationSelectorPresenter( @Assisted private val localMedia: LocalMedia, + @Assisted private val sendAsFile: Boolean, private val maxUploadSizeProvider: MaxUploadSizeProvider, private val featureFlagService: FeatureFlagService, private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider, + private val videoCompressionPresetSelector: VideoCompressionPresetSelector, mediaExtractorFactory: VideoMetadataExtractor.Factory, ) : MediaOptimizationSelectorPresenter { @ContributesBinding(SessionScope::class) @@ -47,6 +49,7 @@ class DefaultMediaOptimizationSelectorPresenter( interface Factory : MediaOptimizationSelectorPresenter.Factory { override fun create( localMedia: LocalMedia, + sendAsFile: Boolean, ): DefaultMediaOptimizationSelectorPresenter } @@ -55,7 +58,9 @@ class DefaultMediaOptimizationSelectorPresenter( @Composable override fun present(): MediaOptimizationSelectorState { val displayMediaSelectorViews by produceState(null) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality) + // When sending as a raw file, never show the optimization selector: images skip + // recompression, while videos use the highest available best-fit preset. + value = !sendAsFile && featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality) } var displayVideoPresetSelectorDialog by remember { mutableStateOf(false) } @@ -123,12 +128,23 @@ class DefaultMediaOptimizationSelectorPresenter( var selectedVideoOptimizationPreset by remember { mutableStateOf>(AsyncData.Loading()) } LaunchedEffect(videoSizeEstimations.dataOrNull()) { + if (sendAsFile) { + // Send-as-file path: pin to no image compression, and pick the highest-quality + // video preset that still fits the upload limit (we have no true "do not re-encode + // video" path in the pre-processor right now). + selectedImageOptimization = AsyncData.Success(false) + selectedVideoOptimizationPreset = videoCompressionPresetSelector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.HIGH, + videoSizeEstimations = videoSizeEstimations, + ) + return@LaunchedEffect + } val mediaOptimizationConfig = mediaOptimizationConfigProvider.get() selectedImageOptimization = AsyncData.Success(mediaOptimizationConfig.compressImages) // Find the best video preset based on the default preset and the video size estimations // Since the estimation for the current preset may be way too large to upload, we check the ones that provide lower file sizes - selectedVideoOptimizationPreset = findBestVideoPreset( - defaultVideoPreset = mediaOptimizationConfig.videoCompressionPreset, + selectedVideoOptimizationPreset = videoCompressionPresetSelector.selectBestVideoPreset( + expectedVideoPreset = mediaOptimizationConfig.videoCompressionPreset, videoSizeEstimations = videoSizeEstimations, ) } @@ -176,20 +192,4 @@ class DefaultMediaOptimizationSelectorPresenter( eventSink = ::handleEvent, ) } - - private fun findBestVideoPreset( - defaultVideoPreset: VideoCompressionPreset, - videoSizeEstimations: AsyncData>, - ): AsyncData { - val estimations = videoSizeEstimations.dataOrNull() ?: return AsyncData.Loading() - // This will find the best video preset that can be used to produce a video that can be uploaded - val bestEstimation = estimations.find { it.preset.ordinal >= defaultVideoPreset.ordinal && it.canUpload }?.preset - return if (bestEstimation != null) { - AsyncData.Success(bestEstimation) - } else { - AsyncData.Failure( - IllegalStateException("No suitable video preset found for default preset: $defaultVideoPreset") - ) - } - } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt index 80cdfd9467..f1e17ef0a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/MediaOptimizationSelectorPresenter.kt @@ -15,6 +15,7 @@ fun interface MediaOptimizationSelectorPresenter : Presenter>, + ): AsyncData { + val estimations = videoSizeEstimations.dataOrNull() ?: return AsyncData.Loading() + val bestEstimation = estimations.find { it.preset.ordinal >= expectedVideoPreset.ordinal && it.canUpload }?.preset + return if (bestEstimation != null) { + AsyncData.Success(bestEstimation) + } else { + AsyncData.Failure( + IllegalStateException("No suitable video preset found for expected preset: $expectedVideoPreset") + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt index a6945b5ebe..b0ff50dc72 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/VideoMetadataExtractor.kt @@ -30,7 +30,6 @@ interface VideoMetadataExtractor : AutoCloseable { } } -@ContributesBinding(AppScope::class) @AssistedInject class DefaultVideoMetadataExtractor( @ApplicationContext private val context: Context, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt index 47d1947766..be53de5f66 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt @@ -11,6 +11,7 @@ package io.element.android.features.messages.impl.crypto.identity import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.USER_NAME_ALICE 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.ui.room.IdentityRoomMember @@ -32,7 +33,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider { override val values: Sequence @@ -37,10 +38,10 @@ fun aResolveVerifiedUserSendFailureState( eventSink = eventSink ) -fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice.FromOther( +fun anUnsignedDeviceSendFailure(userDisplayName: String = USER_NAME_ALICE) = VerifiedUserSendFailure.UnsignedDevice.FromOther( userDisplayName = userDisplayName, ) -fun aChangedIdentitySendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.ChangedIdentity( +fun aChangedIdentitySendFailure(userDisplayName: String = USER_NAME_ALICE) = VerifiedUserSendFailure.ChangedIdentity( userDisplayName = userDisplayName, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index 405e2b0be9..4f7523cbf3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -34,7 +34,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.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem -import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.Text @@ -106,25 +105,21 @@ private fun AttachmentSourcePickerMenu( modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.PhotoFromCamera) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.TakePhoto())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, - style = ListItemStyle.Primary, ) ListItem( modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.VideoFromCamera) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VideoCall())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, - style = ListItemStyle.Primary, ) ListItem( modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.FromGallery) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, - style = ListItemStyle.Primary, ) ListItem( modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.PickAttachmentSource.FromFiles) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Attachment())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, - style = ListItemStyle.Primary, ) if (state.canShareLocation) { ListItem( @@ -134,7 +129,6 @@ private fun AttachmentSourcePickerMenu( }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.LocationPin())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_location)) }, - style = ListItemStyle.Primary, ) } ListItem( @@ -144,14 +138,12 @@ private fun AttachmentSourcePickerMenu( }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Polls())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, - style = ListItemStyle.Primary, ) if (enableTextFormatting) { ListItem( modifier = Modifier.clickable { state.eventSink(MessageComposerEvent.ToggleTextFormatting(enabled = true)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.TextFormatting())), headlineContent = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) }, - style = ListItemStyle.Primary, ) } } 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 90b91691a9..4b9ee28e35 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 @@ -49,6 +49,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.di.annotations.SessionCoroutineScope 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.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkParser @@ -57,7 +58,6 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom 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.room.getDirectRoomMember -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.use import io.element.android.libraries.matrix.api.timeline.TimelineException import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId @@ -110,7 +110,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, + @Assisted private val threadRoot: ThreadId?, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val room: JoinedRoom, private val mediaPickerProvider: PickerProvider, @@ -139,10 +139,11 @@ class MessageComposerPresenter( fun create( timelineController: TimelineController, navigator: MessagesNavigator, - isInThread: Boolean, + threadRoot: ThreadId?, ): MessageComposerPresenter } + private val isInThread = threadRoot != null private val mediaSender = mediaSenderFactory.create(timelineMode = timelineController.mainTimelineMode()) private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) @@ -180,7 +181,7 @@ class MessageComposerPresenter( handlePickedMedia(uri, mimeType) } val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> - handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream) + handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> handlePickedMedia(uri, MimeTypes.Jpeg) @@ -235,8 +236,7 @@ class MessageComposerPresenter( LaunchedEffect(Unit) { val draft = draftService.loadDraft( roomId = room.roomId, - // TODO support threads in composer - threadRoot = null, + threadRoot = threadRoot, isVolatile = false ) if (draft != null) { @@ -572,7 +572,7 @@ class MessageComposerPresenter( notificationConversationService.onSendMessage( sessionId = room.sessionId, roomId = roomInfo.id, - roomName = roomInfo.name ?: roomInfo.id.value, + roomName = roomInfo.name, roomIsDirect = roomInfo.isDm, roomAvatarUrl = roomInfo.avatarUrl ?: roomMembers.getDirectRoomMember(roomInfo = roomInfo, sessionId = room.sessionId)?.avatarUrl, ) @@ -606,6 +606,7 @@ class MessageComposerPresenter( private fun handlePickedMedia( uri: Uri?, mimeType: String? = null, + sendAsFile: Boolean = false, ) { uri ?: return val localMedia = localMediaFactory.createFromUri( @@ -614,7 +615,7 @@ class MessageComposerPresenter( name = null, formattedFileSize = null ) - val mediaAttachment = Attachment.Media(localMedia) + val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile) val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId navigator.navigateToPreviewAttachments(persistentListOf(mediaAttachment), inReplyToEventId) @@ -652,8 +653,7 @@ class MessageComposerPresenter( roomId = room.roomId, draft = draft, isVolatile = isVolatile, - // TODO support threads in composer - threadRoot = null, + threadRoot = threadRoot, ) } @@ -816,8 +816,7 @@ class MessageComposerPresenter( // Use the volatile draft only when coming from edit mode otherwise. val draft = draftService.loadDraft( roomId = room.roomId, - // TODO support threads in composer - threadRoot = null, + threadRoot = threadRoot, isVolatile = true ).takeIf { fromEdit } if (draft != null) { 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 678ef2ba56..ef2362c794 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 @@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType.Ro 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 +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.RoomAlias @@ -189,6 +190,7 @@ internal fun SuggestionsPickerViewPreview() { isIgnored = false, role = RoomMember.Role.User, membershipChangeReason = null, + isServiceMember = false, ) val anAlias = remember { RoomAlias("#room:domain.org") } SuggestionsPickerView( @@ -198,7 +200,7 @@ internal fun SuggestionsPickerViewPreview() { suggestions = persistentListOf( ResolvedSuggestion.AtRoom, ResolvedSuggestion.Member(roomMember), - ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")), + ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = USER_NAME_BOB)), ResolvedSuggestion.Alias( roomAlias = anAlias, roomId = RoomId("!room:matrix.org"), 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 cddc1831db..a618117950 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 @@ -38,6 +38,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive @ContributesNode(RoomScope::class) @AssistedInject @@ -50,7 +52,7 @@ class PinnedMessagesListNode( private val permalinkParser: PermalinkParser, ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { interface Callback : Plugin { - fun handleEventClick(event: TimelineItem.Event) + fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) fun navigateToRoomMemberDetails(userId: UserId) fun viewInTimeline(eventId: EventId) fun handlePermalinkClick(data: PermalinkData.RoomLink) @@ -103,6 +105,7 @@ class PinnedMessagesListNode( @Composable override fun View(modifier: Modifier) { + val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -113,7 +116,9 @@ class PinnedMessagesListNode( PinnedMessagesListView( state = state, onBackClick = ::navigateUp, - onEventClick = callback::handleEventClick, + onEventClick = { + callback.handleEventClick(it, canUseOverlay) + }, onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) }, onLinkClick = { link -> onLinkClick(context, link.url) }, onLinkLongClick = { 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 fd19656fdc..d80243d135 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 @@ -46,7 +46,6 @@ 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.room.JoinedRoom -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId @@ -227,7 +226,8 @@ class PinnedMessagesListPresenter( combine(timelineItemsFlow, room.membersStateFlow) { items, membersState -> timelineItemsFactory.replaceWith( timelineItems = items, - roomMembers = membersState.roomMembers().orEmpty() + roomMembers = membersState.roomMembers().orEmpty(), + renderReadReceipts = false, ) }.launchIn(this) 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 c900aa9f66..4955b12a0f 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 @@ -237,7 +237,6 @@ private fun PinnedMessagesListLoaded( timelineItem = timelineItem, timelineMode = Timeline.Mode.PinnedEvents, timelineRoomInfo = state.timelineRoomInfo, - renderReadReceipts = false, timelineProtectionState = state.timelineProtectionState, isLastOutgoingMessage = false, focusedEventId = null, @@ -311,7 +310,7 @@ private fun TimelineItemEventContentViewWrapper( } else { TimelineItemEventContentView( content = event.content, - hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, 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 0949237862..f476e1f4b8 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 @@ -67,7 +67,9 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -import io.element.android.libraries.mediaplayer.api.MediaPlayer +import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.collections.immutable.ImmutableList @@ -86,7 +88,6 @@ class ThreadedMessagesNode( private val presenterFactory: MessagesPresenter.Factory, private val actionListPresenterFactory: ActionListPresenter.Factory, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, - private val mediaPlayer: MediaPlayer, private val permalinkParser: PermalinkParser, private val appNavigationStateService: AppNavigationStateService, private val roomMemberModerationRenderer: RoomMemberModerationRenderer, @@ -112,7 +113,7 @@ class ThreadedMessagesNode( this.timelineController = timelineController return presenterFactory.create( navigator = this, - composerPresenter = messageComposerPresenterFactory.create(timelineController, this, isInThread = true), + composerPresenter = messageComposerPresenterFactory.create(timelineController, this, threadRoot = inputs.threadRootEventId), timelinePresenter = timelinePresenterFactory.create(timelineController = timelineController, this), // TODO add special processor for threaded timeline actionListPresenter = actionListPresenterFactory.create( @@ -124,7 +125,7 @@ class ThreadedMessagesNode( } interface Callback : Plugin { - fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean + fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean fun navigateToPreviewAttachments(attachments: ImmutableList, inReplyToEventId: EventId?) fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData) @@ -134,9 +135,12 @@ class ThreadedMessagesNode( fun navigateToSendLocation() fun navigateToCreatePoll() fun navigateToEditPoll(eventId: EventId) + fun navigateToCurrentLiveLocation() fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean) fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?) fun navigateToDeveloperSettings() + + fun navigateToAvatarPreview(username: String, avatarUrl: String) } override fun onBuilt() { @@ -154,9 +158,6 @@ class ThreadedMessagesNode( onStop = { appNavigationStateService.onLeavingThread(id) }, - onDestroy = { - mediaPlayer.close() - } ) } @@ -246,12 +247,18 @@ class ThreadedMessagesNode( callback.navigateToDeveloperSettings() } + override fun navigateToCurrentLiveLocation() { + // Shouldn't happen because LiveLocationSharingBanner is not shown in threads. + callback.navigateToCurrentLiveLocation() + } + override fun close() = navigateUp() @Composable override fun View(modifier: Modifier) { val activity = requireNotNull(LocalActivity.current) val isDark = ElementTheme.isLightTheme.not() + val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard() CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -271,11 +278,11 @@ class ThreadedMessagesNode( onEventContentClick = { isLive, event -> timelineController?.let { controller -> if (isLive) { - callback.handleEventClick(controller.mainTimelineMode(), event) + callback.handleEventClick(controller.mainTimelineMode(), event, canUseOverlay) } else { val detachedTimelineMode = controller.detachedTimelineMode() if (detachedTimelineMode != null) { - callback.handleEventClick(detachedTimelineMode, event) + callback.handleEventClick(detachedTimelineMode, event, canUseOverlay) } else { false } @@ -311,6 +318,11 @@ class ThreadedMessagesNode( else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, + onAvatarClick = { user -> + user.avatarUrl?.let { url -> + callback.navigateToAvatarPreview(user.getBestName(), url) + } + }, modifier = Modifier, ) 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 index 21946510d4..07c0838a2b 100644 --- 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 @@ -101,9 +101,10 @@ class ThreadsListPresenter( coroutineScope.launch { Timber.d("Paginating thread list: $paginationStatus") threadsListService.paginate() + .onFailure { Timber.w(it, "Failed to paginate thread list") } } } else { - Timber.d("Not paginating since there is nothing else to load, current status: $paginationStatus") + Timber.d("Not paginating, current status: $paginationStatus") } } } 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 index c93af5c162..5494a9e844 100644 --- 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 @@ -28,9 +28,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -43,6 +47,8 @@ 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.preview.ROOM_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE 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 @@ -77,7 +83,18 @@ fun ThreadsListView( topBar = { TopAppBar( title = { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + val description = stringResource( + CommonStrings.a11y_threads_in_room, + state.roomName, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clearAndSetSemantics { + heading() + contentDescription = description + }, + ) { Avatar( avatarData = AvatarData( id = state.roomId.value, @@ -141,18 +158,19 @@ private fun ScrollHelper( listState: LazyListState, onPaginate: () -> Unit, ) { + val updatedOnPaginate by rememberUpdatedState(onPaginate) val lastVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex + listState.layoutInfo.visibleItemsInfo.size - 1 } } - val needsPagination by remember { + val shouldPaginate by remember { derivedStateOf { - val canLoadNewItems = listState.isScrollInProgress || listState.firstVisibleItemScrollOffset == 0 - canLoadNewItems && lastVisibleItemIndex == listState.layoutInfo.totalItemsCount - 1 + val canLoadNewItems = listState.isScrollInProgress || listState.layoutInfo.totalItemsCount == 0 + canLoadNewItems && lastVisibleItemIndex >= listState.layoutInfo.totalItemsCount - 1 } } - LaunchedEffect(needsPagination, lastVisibleItemIndex) { - if (needsPagination) { - onPaginate() + LaunchedEffect(shouldPaginate, lastVisibleItemIndex) { + if (shouldPaginate) { + updatedOnPaginate() delay(400L) } } @@ -303,7 +321,7 @@ internal fun ThreadsListViewPreview() { ThreadsListView( state = ThreadsListState( roomId = RoomId("!room-id:server"), - roomName = "Room name", + roomName = ROOM_NAME, roomAvatarUrl = null, threads = List(10) { aThreadListRowItem(threadId = ThreadId("\$thread-$it")) }.toImmutableList(), isRoomTombstoned = false, @@ -360,7 +378,7 @@ fun aThreadListItem( 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), + senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null), isOwn: Boolean = false, content: EventContent = MessageContent( body = "Hello world!", 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 edd8d446dc..6711a7fe25 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 @@ -23,6 +23,7 @@ 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.location.api.live.ActiveLiveLocationShareManager import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvent @@ -48,7 +49,6 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.asEventId 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.permissionsAsState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.ReceiptType @@ -66,6 +66,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -94,6 +95,7 @@ class TimelinePresenter( private val roomCallStatePresenter: Presenter, private val featureFlagService: FeatureFlagService, private val analyticsService: AnalyticsService, + private val liveLocationShareManager: ActiveLiveLocationShareManager, ) : Presenter { private val tag = "TimelinePresenter" @@ -136,12 +138,6 @@ class TimelinePresenter( val messageShieldDialogData: MutableState = remember { mutableStateOf(null) } val resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailurePresenter.present() - val isSendPublicReadReceiptsEnabled by remember { - sessionPreferencesStore.isSendPublicReadReceiptsEnabled() - }.collectAsState(initial = true) - val renderReadReceipts by remember { - sessionPreferencesStore.isRenderReadReceiptsEnabled() - }.collectAsState(initial = true) val isLive by remember { timelineController.isLive() }.collectAsState(initial = true) @@ -149,9 +145,6 @@ 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) { @@ -170,12 +163,15 @@ class TimelinePresenter( newEventState.value = NewEventState.None } Timber.tag(tag).d("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}") - sessionCoroutineScope.sendReadReceiptIfNeeded( - firstVisibleIndex = event.firstIndex, - timelineItems = timelineItems, - lastReadReceiptId = lastReadReceiptId, - readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, - ) + sessionCoroutineScope.launch { + val sendPublicReadReceipts = sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first() + sendReadReceiptIfNeeded( + firstVisibleIndex = event.firstIndex, + timelineItems = timelineItems, + lastReadReceiptId = lastReadReceiptId, + readReceiptType = if (sendPublicReadReceipts) ReceiptType.READ else ReceiptType.READ_PRIVATE, + ) + } } else { newEventState.value = NewEventState.None } @@ -200,7 +196,9 @@ class TimelinePresenter( is TimelineEvent.EditPoll -> { navigator.navigateToEditPoll(event.pollStartId) } - is TimelineEvent.StopLiveLocationShare -> Unit + is TimelineEvent.StopLiveLocationShare -> sessionCoroutineScope.launch { + liveLocationShareManager.stopShare(room.roomId) + } is TimelineEvent.FocusOnEvent -> sessionCoroutineScope.launch { focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce) delay(event.debounce) @@ -251,13 +249,18 @@ class TimelinePresenter( } .launchIn(this) - combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState -> + combine( + timelineController.timelineItems(), + room.membersStateFlow, + sessionPreferencesStore.isRenderReadReceiptsEnabled(), + ) { items, membersState, renderReadReceipts -> val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems) val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items") transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString()) timelineItemsFactory.replaceWith( timelineItems = items, - roomMembers = membersState.roomMembers().orEmpty() + roomMembers = membersState.roomMembers().orEmpty(), + renderReadReceipts = renderReadReceipts, ) transaction?.finish() items @@ -312,14 +315,12 @@ class TimelinePresenter( timelineItems = timelineItems, timelineMode = timelineMode, timelineRoomInfo = timelineRoomInfo, - renderReadReceipts = renderReadReceipts, newEventState = newEventState.value, isLive = isLive, focusRequestState = focusRequestState.value, 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 1869ad6906..d2f0faaa24 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 @@ -26,7 +26,6 @@ data class TimelineState( val timelineItems: ImmutableList, val timelineRoomInfo: TimelineRoomInfo, val timelineMode: Timeline.Mode, - val renderReadReceipts: Boolean, val newEventState: NewEventState, val isLive: Boolean, val focusRequestState: FocusRequestState, @@ -34,7 +33,6 @@ 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 9840ac5107..bd0b093ed5 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 @@ -29,6 +29,8 @@ import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.roomcall.api.aStandByCallState 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.ROOM_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_SENDER import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UniqueId @@ -49,14 +51,12 @@ import kotlin.random.Random fun aTimelineState( timelineItems: ImmutableList = persistentListOf(), timelineMode: Timeline.Mode = Timeline.Mode.Live, - renderReadReceipts: Boolean = false, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), focusedEventIndex: Int = -1, isLive: Boolean = true, messageShield: MessageShield? = null, resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(), displayThreadSummaries: Boolean = false, - displayFloatingDateBadge: Boolean = false, eventSink: (TimelineEvent) -> Unit = {}, ): TimelineState { val focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId @@ -69,14 +69,12 @@ fun aTimelineState( timelineItems = timelineItems, timelineMode = timelineMode, timelineRoomInfo = timelineRoomInfo, - renderReadReceipts = renderReadReceipts, newEventState = NewEventState.None, isLive = isLive, focusRequestState = focusRequestState, messageShieldDialogData = messageShield?.let { MessageShieldData(it) }, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, displayThreadSummaries = displayThreadSummaries, - displayFloatingDateBadge = displayFloatingDateBadge, eventSink = eventSink, ) } @@ -143,7 +141,7 @@ internal fun aTimelineItemEvent( isMine: Boolean = false, isEditable: Boolean = false, canBeRepliedTo: Boolean = false, - senderDisplayName: String = "Sender", + senderDisplayName: String = USER_NAME_SENDER, displayNameAmbiguous: Boolean = false, content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, @@ -160,7 +158,7 @@ internal fun aTimelineItemEvent( eventId = eventId, transactionId = transactionId, senderId = UserId("@senderId:domain"), - senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender), + senderAvatar = AvatarData("@senderId:domain", USER_NAME_SENDER, size = AvatarSize.TimelineSender), content = content, reactionsState = timelineItemReactions, readReceiptState = readReceiptState, @@ -253,7 +251,7 @@ internal fun aGroupedEvents( } internal fun aTimelineRoomInfo( - name: String = "Room name", + name: String = ROOM_NAME, isDm: Boolean = false, userHasPermissionToSendMessage: Boolean = true, pinnedEventIds: List = emptyList(), 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 2105cf9df7..0a306aea1c 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 @@ -77,7 +77,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -171,7 +171,6 @@ fun TimelineView( timelineMode = state.timelineMode, timelineRoomInfo = state.timelineRoomInfo, timelineProtectionState = timelineProtectionState, - renderReadReceipts = state.renderReadReceipts, isLastOutgoingMessage = state.isLastOutgoingMessage(timelineItem.identifier()), focusedEventId = state.focusedEventId, displayThreadSummaries = state.displayThreadSummaries, @@ -213,7 +212,7 @@ fun TimelineView( onFocusEventRender = ::onFocusEventRender, ) - if (state.displayFloatingDateBadge && useReverseLayout) { + if (useReverseLayout) { FloatingDateBadgeOverlay( lazyListState = lazyListState, timelineItems = state.timelineItems, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 6be00fc14a..45f4a86a53 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -22,7 +22,6 @@ internal fun ATimelineItemEventRow( event: TimelineItem.Event, timelineMode: Timeline.Mode = Timeline.Mode.Live, timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), - renderReadReceipts: Boolean = false, isLastOutgoingMessage: Boolean = false, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), displayThreadSummaries: Boolean = false, @@ -30,7 +29,6 @@ internal fun ATimelineItemEventRow( event = event, timelineMode = timelineMode, timelineRoomInfo = timelineRoomInfo, - renderReadReceipts = renderReadReceipts, timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, displayThreadSummaries = displayThreadSummaries, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index aa5aaa2075..80bd342d01 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -47,8 +47,8 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground import io.element.android.libraries.designsystem.theme.messageFromOtherBackground import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.libraries.ui.utils.graphics.drawInLayer -import io.element.android.libraries.ui.utils.time.isTalkbackActive private val BUBBLE_RADIUS = 12.dp private val avatarRadius = AvatarSize.TimelineSender.dp / 2 diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index 096ab018e2..1859e66750 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -26,6 +26,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.R import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId @@ -162,7 +163,7 @@ internal fun MessageShieldViewPreview() { MessageShield.AuthenticityNotGuaranteed(false), forwarder = UserId("@alice:example.com"), forwarderProfile = ProfileDetails.Ready( - displayName = "Alice", + displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null, ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index 21ef7c5b09..25a53cec2d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -15,9 +15,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -47,9 +50,39 @@ fun TimelineEventTimestampView( val isMessageEdited = event.content.isEdited() val isMessageRedacted = event.content.isRedacted() val tint = if (hasError || hasEncryptionCritical && !isMessageRedacted) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary + + val shield = event.messageShield + val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser + val onClickLabel = when { + shield != null -> stringResource(CommonStrings.a11y_view_details) + hasError && isVerifiedUserSendFailure -> stringResource(CommonStrings.action_open_context_menu) + else -> null + } + val clickableModifier = remember(shield, hasError) { + when { + shield != null -> { + Modifier.clickable( + onClickLabel = onClickLabel, + ) { + eventSink(TimelineEvent.ShowShieldDialog(shield)) + } + } + hasError -> Modifier + .clickable( + enabled = isVerifiedUserSendFailure, + onClickLabel = onClickLabel, + ) { + eventSink(TimelineEvent.ComputeVerifiedUserSendFailure(event)) + } + else -> Modifier + } + } Row( modifier = Modifier .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) + // For a better click target, make the corners rounded + .clip(RoundedCornerShape(8.dp)) + .then(clickableModifier) .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { @@ -67,36 +100,22 @@ fun TimelineEventTimestampView( color = tint, ) if (hasError) { - val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = CompoundIcons.ErrorSolid(), contentDescription = stringResource(id = CommonStrings.common_sending_failed), tint = tint, - modifier = Modifier - .size(15.dp, 18.dp) - .clickable( - enabled = isVerifiedUserSendFailure, - onClickLabel = stringResource(CommonStrings.action_open_context_menu), - ) { - eventSink(TimelineEvent.ComputeVerifiedUserSendFailure(event)) - } + modifier = Modifier.size(15.dp, 18.dp), ) } if (!isMessageRedacted) { - event.messageShield?.let { shield -> + shield?.let { shield -> Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = shield.toIcon(), contentDescription = stringResource(id = CommonStrings.a11y_encryption_details), - modifier = Modifier - .size(15.dp) - .clickable( - onClickLabel = stringResource(CommonStrings.a11y_view_details), - ) { - eventSink(TimelineEvent.ShowShieldDialog(shield)) - }, + modifier = Modifier.size(15.dp), tint = shield.toIconColor(), ) Spacer(modifier = Modifier.width(4.dp)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt index dfd1b2ed0e..a7ab44db54 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemCallNotifyView.kt @@ -8,6 +8,7 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.annotation.StringRes import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -22,17 +23,23 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo +import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState +import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView +import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -42,80 +49,129 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun TimelineItemCallNotifyView( + timelineRoomInfo: TimelineRoomInfo, event: TimelineItem.Event, content: TimelineItemRtcNotificationContent, + isLastOutgoingMessage: Boolean, onLongClick: (TimelineItem.Event) -> Unit, + onReadReceiptsClick: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier ) { - Row( - modifier = modifier - .fillMaxWidth() - .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp)) - .combinedClickable( - enabled = true, - onClick = {}, - onLongClick = { onLongClick(event) }, - onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + Column(modifier = modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + .border(1.dp, ElementTheme.colors.borderInteractiveSecondary, RoundedCornerShape(8.dp)) + .combinedClickable( + enabled = true, + onClick = {}, + onLongClick = { onLongClick(event) }, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) + .onKeyboardContextMenuAction { onLongClick(event) } + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(20.sp.toDp()), + imageVector = getIcon(timelineRoomInfo, content), + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, ) - .onKeyboardContextMenuAction { onLongClick(event) } - .padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Avatar( - avatarData = event.senderAvatar, - avatarType = AvatarType.User, - ) - Column(modifier = Modifier.weight(1f)) { + Text( - text = event.safeSenderName, - style = ElementTheme.typography.fontBodyLgMedium, + modifier = Modifier.weight(1f), + text = stringResource(getTextRes(timelineRoomInfo, content)), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = event.sentTime, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier.size(20.sp.toDp()), - imageVector = - if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(), - contentDescription = null, - tint = ElementTheme.colors.iconSecondary, - ) - Text( - text = stringResource(CommonStrings.common_call_started), - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } } - Text( - text = event.sentTime, - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + + TimelineItemReadReceiptView( + state = ReadReceiptViewState( + sendState = event.localSendState, + isLastOutgoingMessage = isLastOutgoingMessage, + receipts = event.readReceiptState.receipts, + ), + onReadReceiptsClick = { onReadReceiptsClick(event) }, + modifier = Modifier.padding(top = 4.dp), ) } } +@StringRes +private fun getTextRes( + timelineRoomInfo: TimelineRoomInfo, + content: TimelineItemRtcNotificationContent +): Int = if (timelineRoomInfo.isDm) { + when (content.state) { + is RtcNotificationState.Declined -> { + if (content.state.byMe) CommonStrings.common_call_you_declined else CommonStrings.common_call_declined + } + RtcNotificationState.Started -> CommonStrings.common_call_started + } +} else { + // In Rooms, do not show declined info. + CommonStrings.common_call_started +} + +@Composable +private fun getIcon( + timelineRoomInfo: TimelineRoomInfo, + content: TimelineItemRtcNotificationContent +): ImageVector { + val showAsDeclined = timelineRoomInfo.isDm && content.state is RtcNotificationState.Declined + val icon = if (showAsDeclined) { + if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallDeclinedSolid() else CompoundIcons.VideoCallDeclinedSolid() + } else { + if (content.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid() + } + return icon +} + @PreviewsDayNight @Composable internal fun TimelineItemCallNotifyViewPreview() = ElementPreview { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - listOf( - TimelineItemRtcNotificationContent(CallIntent.AUDIO), - TimelineItemRtcNotificationContent(CallIntent.VIDEO), - ).forEach { content -> - TimelineItemCallNotifyView( - event = aTimelineItemEvent(content = content), - content = content, - onLongClick = {}, - ) + val readReceiptState = mutableListOf( + aTimelineItemReadReceipts( + receipts = List(3) { aReadReceiptData(it) }, + ) + ) + Column(modifier = Modifier.padding(bottom = 16.dp)) { + listOf(false, true).forEach { isDm -> + listOf(CallIntent.AUDIO, CallIntent.VIDEO).forEach { callIntent -> + listOf( + RtcNotificationState.Started, + RtcNotificationState.Declined(byMe = false), + RtcNotificationState.Declined(byMe = true), + ).forEach { state -> + val content = TimelineItemRtcNotificationContent(callIntent, state) + TimelineItemCallNotifyView( + timelineRoomInfo = aTimelineRoomInfo(isDm = isDm), + event = aTimelineItemEvent( + content = content, + // Only display read receipts for the first item + readReceiptState = readReceiptState.removeFirstOrNull() ?: aTimelineItemReadReceipts(), + ), + content = content, + isLastOutgoingMessage = false, + onLongClick = {}, + onReadReceiptsClick = {}, + ) + } + } } } } 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 976fa3c17e..1f7f127be7 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 @@ -92,6 +92,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.modifiers.niceClickable import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.swipe.SwipeableActionsState import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState import io.element.android.libraries.designsystem.text.toPx @@ -120,7 +121,7 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link import kotlinx.coroutines.launch import kotlin.math.abs @@ -141,7 +142,6 @@ fun TimelineItemEventRow( timelineMode: Timeline.Mode, timelineRoomInfo: TimelineRoomInfo, timelineProtectionState: TimelineProtectionState, - renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, displayThreadSummaries: Boolean, onEventClick: () -> Unit, @@ -295,7 +295,6 @@ fun TimelineItemEventRow( isLastOutgoingMessage = isLastOutgoingMessage, receipts = event.readReceiptState.receipts, ), - renderReadReceipts = renderReadReceipts, onReadReceiptsClick = { onReadReceiptClick(event) }, modifier = Modifier.padding(top = 4.dp) ) @@ -782,7 +781,7 @@ private fun MessageEventBubbleContent( val content = content.ensureActiveLiveLocation() val shouldHide = content.mode is TimelineItemLocationContent.Mode.Live && content.mode.isActive && - content.mode.canStop + content.mode.isOwnUser if (shouldHide) TimestampPosition.Hidden else TimestampPosition.Overlay } is TimelineItemPollContent -> TimestampPosition.Below @@ -863,7 +862,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview { ), senderId = UserId("@user:id"), senderProfile = ProfileDetails.Ready( - displayName = "Alice", + displayName = USER_NAME_ALICE, avatarUrl = null, displayNameAmbiguous = false, ), @@ -898,7 +897,7 @@ internal fun ThreadSummaryViewPreview() { ), senderId = UserId("@user:id"), senderProfile = ProfileDetails.Ready( - displayName = "Alice", + displayName = USER_NAME_ALICE, avatarUrl = null, displayNameAmbiguous = true, ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowUtdPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowUtdPreview.kt index 49328ac025..abacc45ef8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowUtdPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowUtdPreview.kt @@ -16,6 +16,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause @@ -25,7 +27,7 @@ internal fun TimelineItemEventRowUtdPreview() = ElementPreview { Column { ATimelineItemEventRow( event = aTimelineItemEvent( - senderDisplayName = "Alice", + senderDisplayName = USER_NAME_ALICE, isMine = false, content = TimelineItemEncryptedContent( data = UnableToDecryptContent.Data.MegolmV1AesSha2( @@ -39,7 +41,7 @@ internal fun TimelineItemEventRowUtdPreview() = ElementPreview { ) ATimelineItemEventRow( event = aTimelineItemEvent( - senderDisplayName = "Bob", + senderDisplayName = USER_NAME_BOB, isMine = false, content = TimelineItemEncryptedContent( data = UnableToDecryptContent.Data.MegolmV1AesSha2( @@ -54,7 +56,7 @@ internal fun TimelineItemEventRowUtdPreview() = ElementPreview { ATimelineItemEventRow( event = aTimelineItemEvent( - senderDisplayName = "Bob", + senderDisplayName = USER_NAME_BOB, isMine = false, content = TimelineItemEncryptedContent( data = UnableToDecryptContent.Data.MegolmV1AesSha2( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt index f46ba780f3..6ea1161605 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt @@ -37,7 +37,6 @@ internal fun TimelineItemEventRowWithRRPreview( timelineItemReactions = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(state.receipts), ), - renderReadReceipts = true, isLastOutgoingMessage = false, ) // A message from current user @@ -49,7 +48,6 @@ internal fun TimelineItemEventRowWithRRPreview( timelineItemReactions = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(state.receipts), ), - renderReadReceipts = true, isLastOutgoingMessage = false, ) // Another message from current user @@ -61,7 +59,6 @@ internal fun TimelineItemEventRowWithRRPreview( timelineItemReactions = aTimelineItemReactions(count = 0), readReceiptState = TimelineItemReadReceipts(state.receipts), ), - renderReadReceipts = true, isLastOutgoingMessage = true, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 8316911843..b41e66ae82 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -34,7 +34,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link @Composable @@ -43,7 +43,6 @@ fun TimelineItemGroupedEventsRow( timelineMode: Timeline.Mode, timelineRoomInfo: TimelineRoomInfo, timelineProtectionState: TimelineProtectionState, - renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, focusedEventId: EventId?, displayThreadSummaries: Boolean, @@ -63,7 +62,7 @@ fun TimelineItemGroupedEventsRow( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, @@ -89,7 +88,6 @@ fun TimelineItemGroupedEventsRow( timelineRoomInfo = timelineRoomInfo, timelineProtectionState = timelineProtectionState, focusedEventId = focusedEventId, - renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, displayThreadSummaries = displayThreadSummaries, onClick = onClick, @@ -117,7 +115,6 @@ private fun TimelineItemGroupedEventsRowContent( timelineRoomInfo: TimelineRoomInfo, timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, - renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, displayThreadSummaries: Boolean, onClick: (TimelineItem.Event) -> Unit, @@ -136,7 +133,7 @@ private fun TimelineItemGroupedEventsRowContent( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, @@ -173,7 +170,6 @@ private fun TimelineItemGroupedEventsRowContent( timelineItem = subGroupEvent, timelineRoomInfo = timelineRoomInfo, timelineProtectionState = timelineProtectionState, - renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, displayThreadSummaries = displayThreadSummaries, @@ -193,14 +189,13 @@ private fun TimelineItemGroupedEventsRowContent( ) } } - } else if (renderReadReceipts) { + } else if (timelineItem.aggregatedReadReceipts.isNotEmpty()) { TimelineItemReadReceiptView( state = ReadReceiptViewState( sendState = null, isLastOutgoingMessage = false, receipts = timelineItem.aggregatedReadReceipts, ), - renderReadReceipts = true, onReadReceiptsClick = onExpandGroupClick ) } @@ -219,7 +214,6 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi timelineRoomInfo = aTimelineRoomInfo(), timelineProtectionState = aTimelineProtectionState(), focusedEventId = events.events.first().eventId, - renderReadReceipts = true, isLastOutgoingMessage = false, displayThreadSummaries = false, onClick = {}, @@ -247,7 +241,6 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi timelineRoomInfo = aTimelineRoomInfo(), timelineProtectionState = aTimelineProtectionState(), focusedEventId = null, - renderReadReceipts = true, isLastOutgoingMessage = false, displayThreadSummaries = false, onClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index e75df2f89f..0229d2220b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -47,7 +47,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link import kotlin.time.DurationUnit @@ -56,7 +56,6 @@ internal fun TimelineItemRow( timelineItem: TimelineItem, timelineMode: Timeline.Mode, timelineRoomInfo: TimelineRoomInfo, - renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, timelineProtectionState: TimelineProtectionState, focusedEventId: EventId?, @@ -78,7 +77,7 @@ internal fun TimelineItemRow( { event, contentModifier, onContentLayoutChange -> TimelineItemEventContentView( content = event.content, - hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId), + hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine), onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) }, onContentClick = { onContentClick(event) }, onLongClick = { onLongClick(event) }, @@ -114,7 +113,6 @@ internal fun TimelineItemRow( is TimelineItemStateContent, is TimelineItemLegacyCallInviteContent -> { TimelineItemStateEventRow( event = timelineItem, - renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, onClick = { onContentClick(timelineItem) }, onReadReceiptsClick = onReadReceiptClick, @@ -124,10 +122,12 @@ internal fun TimelineItemRow( } is TimelineItemRtcNotificationContent -> { TimelineItemCallNotifyView( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), + timelineRoomInfo = timelineRoomInfo, event = timelineItem, content = timelineItem.content, + isLastOutgoingMessage = isLastOutgoingMessage, onLongClick = onLongClick, + onReadReceiptsClick = onReadReceiptClick, ) } else -> { @@ -164,7 +164,6 @@ internal fun TimelineItemRow( event = timelineItem, timelineMode = timelineMode, timelineRoomInfo = timelineRoomInfo, - renderReadReceipts = renderReadReceipts, timelineProtectionState = timelineProtectionState, isLastOutgoingMessage = isLastOutgoingMessage, displayThreadSummaries = displayThreadSummaries, @@ -193,7 +192,6 @@ internal fun TimelineItemRow( timelineMode = timelineMode, timelineRoomInfo = timelineRoomInfo, timelineProtectionState = timelineProtectionState, - renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, focusedEventId = focusedEventId, displayThreadSummaries = displayThreadSummaries, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index 6e409f2b8d..db32b7a524 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -39,7 +39,6 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun TimelineItemStateEventRow( event: TimelineItem.Event, - renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, @@ -86,7 +85,6 @@ fun TimelineItemStateEventRow( isLastOutgoingMessage = isLastOutgoingMessage, receipts = event.readReceiptState.receipts, ), - renderReadReceipts = renderReadReceipts, onReadReceiptsClick = { onReadReceiptsClick(event) }, ) } @@ -104,7 +102,6 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview { receipts = persistentListOf(aReadReceiptData(0)), ) ), - renderReadReceipts = true, isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index a8cbb89e96..0e0a98a96c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -54,10 +54,11 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.link.Link +private const val TALL_IMAGE_RATIO_DIVISOR = 3 @Composable fun TimelineItemImageView( content: TimelineItemImageContent, @@ -79,7 +80,7 @@ fun TimelineItemImageView( Modifier } TimelineItemAspectRatioBox( - modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f), + modifier = containerModifier.blurHashBackground(content.blurhash, alpha = 0.9f).align(Alignment.CenterHorizontally), aspectRatio = coerceRatioWhenHidingContent(content.aspectRatio, hideMediaContent), ) { ProtectedView( @@ -123,7 +124,14 @@ fun TimelineItemImageView( LocalContentColor provides ElementTheme.colors.textPrimary, LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular ) { - val aspectRatio = content.aspectRatio ?: DEFAULT_ASPECT_RATIO + val width = content.width ?: 0 + val height = content.height ?: 0 + // if image is narrow and tall use DEFAULT_ASPECT_RATIO + val aspectRatio = if (width < height / TALL_IMAGE_RATIO_DIVISOR) { + DEFAULT_ASPECT_RATIO + } else { + content.aspectRatio ?: DEFAULT_ASPECT_RATIO + } EditorStyledText( modifier = Modifier .padding(horizontal = 4.dp) // This is (12.dp - 8.dp) contentPadding from CommonLayout @@ -200,3 +208,38 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview { ) } } + +@PreviewsDayNight +@Composable +internal fun ATimelineItemEventRowPreview() = ElementPreview { + Column { + sequenceOf(false, true).forEach { isMine -> + ATimelineItemEventRow( + event = aTimelineItemEvent( + isMine = isMine, + content = aTimelineItemImageContent( + filename = "image.jpg", + caption = "A long caption that may wrap into several lines", + width = 80, + height = 300, + aspectRatio = 80f / 300f, + ), + groupPosition = TimelineItemGroupPosition.Last, + ), + ) + } + ATimelineItemEventRow( + event = aTimelineItemEvent( + isMine = false, + content = aTimelineItemImageContent( + filename = "image.jpg", + caption = "Narrow image with null aspectRatio", + width = 80, + height = 300, + aspectRatio = null, + ), + groupPosition = TimelineItemGroupPosition.Last, + ), + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt index f00b6b0b5b..4ab4ee84a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt @@ -13,14 +13,12 @@ import androidx.compose.foundation.border 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.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -77,13 +75,14 @@ private fun LiveLocationOverlay( Row( modifier = modifier .fillMaxWidth() - .background(ElementTheme.colors.bgCanvasDefault.copy(alpha = 0.9f)) - .padding(horizontal = 8.dp, vertical = 8.dp), + .background(ElementTheme.colors.bgCanvasDefault.copy(alpha = 0.9f)), verticalAlignment = Alignment.CenterVertically, ) { val iconShape = RoundedCornerShape(8.dp) Box( modifier = Modifier + // Ensure this Box uses same spacings than the Stop IconButton. + .minimumInteractiveComponentSize() .size(32.dp) .border( width = 1.dp, @@ -120,7 +119,6 @@ private fun LiveLocationOverlay( ) } } - Spacer(Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = if (mode.isActive) { @@ -140,13 +138,16 @@ private fun LiveLocationOverlay( } } - if (mode.isActive && mode.canStop) { + if (mode.canStopSharing) { IconButton( onClick = onStopClick, colors = IconButtonDefaults.iconButtonColors( containerColor = ElementTheme.colors.bgCriticalPrimary, contentColor = ElementTheme.colors.iconOnSolidPrimary, - ) + ), + modifier = Modifier + .minimumInteractiveComponentSize() + .size(30.dp) ) { Icon( imageVector = CompoundIcons.Stop(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index f5e760736e..8d1ef18f39 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.link.Link diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 5dbd0c478f..86e3f1c849 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -52,7 +52,7 @@ 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.ui.strings.CommonStrings -import io.element.android.libraries.ui.utils.time.isTalkbackActive +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index b03aaf01c1..037d9ea45d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -53,27 +53,24 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun TimelineItemReadReceiptView( state: ReadReceiptViewState, - renderReadReceipts: Boolean, onReadReceiptsClick: () -> Unit, modifier: Modifier = Modifier, ) { if (state.receipts.isNotEmpty()) { - if (renderReadReceipts) { - ReadReceiptsRow( - modifier = modifier.clearAndSetSemantics { - hideFromAccessibility() - } - ) { - ReadReceiptsAvatars( - receipts = state.receipts, - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .clickable { - onReadReceiptsClick() - } - .padding(2.dp) - ) + ReadReceiptsRow( + modifier = modifier.clearAndSetSemantics { + hideFromAccessibility() } + ) { + ReadReceiptsAvatars( + receipts = state.receipts, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { + onReadReceiptsClick() + } + .padding(2.dp) + ) } } else { when (state.sendState) { @@ -209,7 +206,6 @@ internal fun TimelineItemReadReceiptViewPreview( ) = ElementPreview { TimelineItemReadReceiptView( state = state, - renderReadReceipts = true, onReadReceiptsClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt index f812b40e61..d59526d8bb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemRoomBeginningView.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.R import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.ROOM_NAME import io.element.android.libraries.designsystem.text.toAnnotatedString import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.allBooleans @@ -86,13 +87,13 @@ internal fun TimelineItemRoomBeginningViewPreview() = ElementPreview { ) TimelineItemRoomBeginningView( predecessorRoom = null, - roomName = "Room Name", + roomName = ROOM_NAME, isDm = isDm, onPredecessorRoomClick = {}, ) TimelineItemRoomBeginningView( predecessorRoom = PredecessorRoom(RoomId("!roomId:matrix.org")), - roomName = "Room Name", + roomName = ROOM_NAME, isDm = isDm, onPredecessorRoomClick = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt index 7c36521fc0..0d51d9f5b8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt @@ -8,7 +8,9 @@ package io.element.android.features.messages.impl.timeline.di +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.timeline.model.event.ensureActiveLiveLocation import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.libraries.voiceplayer.api.aVoiceMessageState @@ -18,6 +20,12 @@ import io.element.android.libraries.voiceplayer.api.aVoiceMessageState */ fun aFakeTimelineItemPresenterFactories() = TimelineItemPresenterFactories( mapOf( + Pair( + TimelineItemLocationContent::class, + TimelineItemPresenterFactory { content -> + Presenter { content.ensureActiveLiveLocation() } + }, + ), Pair( TimelineItemVoiceContent::class, TimelineItemPresenterFactory { Presenter { aVoiceMessageState() } }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index 7b369fe6b7..1679a6030a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -64,22 +64,24 @@ class TimelineItemsFactory( suspend fun replaceWith( timelineItems: List, roomMembers: List, + renderReadReceipts: Boolean, ) = withContext(dispatchers.computation) { lock.withLock { diffCacheUpdater.updateWith(timelineItems) - buildAndEmitTimelineItemStates(timelineItems, roomMembers) + buildAndEmitTimelineItemStates(timelineItems, roomMembers, renderReadReceipts) } } private suspend fun buildAndEmitTimelineItemStates( timelineItems: List, roomMembers: List, + renderReadReceipts: Boolean, ) { val newTimelineItemStates = ArrayList() for (index in diffCache.indices().reversed()) { val cacheItem = diffCache.get(index) if (cacheItem == null) { - buildAndCacheItem(timelineItems, index, roomMembers)?.also { timelineItemState -> + buildAndCacheItem(timelineItems, index, roomMembers, renderReadReceipts)?.also { timelineItemState -> newTimelineItemStates.add(timelineItemState) } } else { @@ -87,7 +89,8 @@ class TimelineItemsFactory( eventItemFactory.update( timelineItem = cacheItem, receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event, - roomMembers = roomMembers + roomMembers = roomMembers, + renderReadReceipts = renderReadReceipts, ) } else { cacheItem @@ -103,10 +106,11 @@ class TimelineItemsFactory( timelineItems: List, index: Int, roomMembers: List, + renderReadReceipts: Boolean, ): TimelineItem? { val timelineItem = when (val currentTimelineItem = timelineItems[index]) { - is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers) + is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers, renderReadReceipts) is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem) MatrixTimelineItem.Other -> null } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index dff195e833..3b3e5118d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline.factories.event import dev.zacsweers.metro.Inject import io.element.android.features.location.api.Location +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -104,7 +105,12 @@ class TimelineItemContentFactory( is PollContent -> pollFactory.create(eventId, isEditable, isOutgoing, itemContent) is UnableToDecryptContent -> utdFactory.create(itemContent) is CallNotifyContent -> TimelineItemRtcNotificationContent( - itemContent.callIntent + callIntent = itemContent.callIntent, + state = if (itemContent.declinedBy.isEmpty()) { + RtcNotificationState.Started + } else { + RtcNotificationState.Declined(itemContent.declinedBy.any { it == sessionId }) + } ) is UnknownContent -> TimelineItemUnknownContent is LiveLocationContent -> { @@ -127,6 +133,7 @@ class TimelineItemContentFactory( isActive = itemContent.isLive, endsAt = stringProvider.getString(CommonStrings.common_ends_at, endsAt), endTimestamp = itemContent.endTimestamp, + isOwnUser = sessionId == sender ), ) } 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 cf515a0b51..a36755dc09 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 @@ -57,6 +57,7 @@ class TimelineItemEventFactory( index: Int, timelineItems: List, roomMembers: List, + renderReadReceipts: Boolean, ): TimelineItem.Event { val currentSender = currentTimelineItem.event.sender val groupPosition = @@ -116,7 +117,7 @@ class TimelineItemEventFactory( sentDate = sentDate, groupPosition = groupPosition, reactionsState = currentTimelineItem.computeReactionsState(), - readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers), + readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts), localSendState = currentTimelineItem.event.localSendState, inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser), threadInfo = mappedThreadInfo, @@ -133,9 +134,10 @@ class TimelineItemEventFactory( timelineItem: TimelineItem.Event, receivedMatrixTimelineItem: MatrixTimelineItem.Event, roomMembers: List, + renderReadReceipts: Boolean, ): TimelineItem.Event { return timelineItem.copy( - readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers) + readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts) ) } @@ -180,8 +182,9 @@ class TimelineItemEventFactory( private fun MatrixTimelineItem.Event.computeReadReceiptState( roomMembers: List, + renderReadReceipts: Boolean, ): TimelineItemReadReceipts { - if (!config.computeReadReceipts) { + if (!config.computeReadReceipts || !renderReadReceipts) { return TimelineItemReadReceipts(receipts = persistentListOf()) } return TimelineItemReadReceipts( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 44dd2df38d..52e008e121 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -27,7 +27,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider Long, ): Boolean { + val updatedCurrentTimeMillis by rememberUpdatedState(currentTimeMillis) fun TimelineItemLocationContent.Mode.Live.isActive(): Boolean { - return isActive && endTimestamp > currentTimeMillis() + return isActive && endTimestamp > updatedCurrentTimeMillis() } return produceState( initialValue = mode.isActive(), @@ -111,7 +115,7 @@ private fun rememberIsLiveLocationActive( key2 = mode.isActive, ) { if (mode.isActive) { - val remainingMillis = mode.endTimestamp - currentTimeMillis() + val remainingMillis = mode.endTimestamp - updatedCurrentTimeMillis() delay(remainingMillis) } value = false diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt index a9cc9e59d4..6c095c828e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt @@ -17,48 +17,44 @@ import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsRead open class TimelineItemLocationContentProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aTimelineItemLocationContent(), aTimelineItemLocationContent( - mode = TimelineItemLocationContent.Mode.Live( - isActive = true, - endsAt = "Ends at 12:34", - endTimestamp = 0L, - canStop = true, - lastKnownLocation = aLocation() - ), + mode = aStaticLocationMode() ), aTimelineItemLocationContent( - mode = TimelineItemLocationContent.Mode.Live( - isActive = true, - endsAt = "Ends at 12:34", - endTimestamp = 0L, - lastKnownLocation = aLocation() - ), + mode = aLiveLocationMode(isActive = true) ), aTimelineItemLocationContent( - mode = TimelineItemLocationContent.Mode.Live( - isActive = true, - endsAt = "Ends at 12:34", - endTimestamp = 0L, - lastKnownLocation = null - ), + mode = aLiveLocationMode(isActive = true, lastKnownLocation = null) ), aTimelineItemLocationContent( - mode = TimelineItemLocationContent.Mode.Live( - isActive = false, - endsAt = "", - endTimestamp = 0L, - lastKnownLocation = aLocation() - ), + mode = aLiveLocationMode(isActive = true, isOwnUser = false) + ), + aTimelineItemLocationContent( + mode = aLiveLocationMode(isActive = false) ), ) } +fun aLiveLocationMode( + isActive: Boolean, + isOwnUser: Boolean = true, + lastKnownLocation: Location? = aLocation(), + endsAt: String = "Ends at 12:34", + endTimestamp: Long = 0L, +): TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Live( + isActive = isActive, + endsAt = endsAt, + endTimestamp = endTimestamp, + isOwnUser = isOwnUser, + lastKnownLocation = lastKnownLocation +) + +fun aStaticLocationMode(location: Location = aLocation()) = TimelineItemLocationContent.Mode.Static(location) fun aTimelineItemLocationContent( senderId: UserId = UserId("@sender:matrix.org"), senderProfile: ProfileDetails = aProfileDetailsReady(), description: String? = null, - mode: TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Static(aLocation()), + mode: TimelineItemLocationContent.Mode, ) = TimelineItemLocationContent( senderId = senderId, senderProfile = senderProfile, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt index 53facfc675..2359f196a9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemRtcNotificationContent.kt @@ -9,7 +9,19 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.timeline.item.event.EventType -class TimelineItemRtcNotificationContent(val callIntent: CallIntent) : TimelineItemEventContent { - override val type: String = "org.matrix.msc4075.rtc.notification" +// State of the call, for now only isDeclined but in the future could be missed, active. +sealed interface RtcNotificationState { + /** Some users have declined, byMe indicates if the current user is one of them. */ + data class Declined(val byMe: Boolean) : RtcNotificationState + + object Started : RtcNotificationState +} + +class TimelineItemRtcNotificationContent( + val callIntent: CallIntent, + val state: RtcNotificationState, +) : TimelineItemEventContent { + override val type: String = EventType.RTC_NOTIFICATION } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt index 5a5363f0c6..fcaa28a108 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineItem.kt @@ -34,26 +34,32 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt */ fun TimelineItem.mustBeProtected(): Boolean { return when (this) { - is TimelineItem.Event -> when (content) { - is TimelineItemImageContent, - is TimelineItemVideoContent, - is TimelineItemStickerContent -> true - is TimelineItemAudioContent, - is TimelineItemRtcNotificationContent, - is TimelineItemEncryptedContent, - is TimelineItemFileContent, - TimelineItemLegacyCallInviteContent, - is TimelineItemLocationContent, - is TimelineItemPollContent, - TimelineItemRedactedContent, - is TimelineItemProfileChangeContent, - is TimelineItemRoomMembershipContent, - is TimelineItemStateEventContent, - is TimelineItemEmoteContent, - is TimelineItemNoticeContent, - is TimelineItemTextContent, - TimelineItemUnknownContent, - is TimelineItemVoiceContent -> false + is TimelineItem.Event -> { + if (isMine) { + false + } else { + when (content) { + is TimelineItemImageContent, + is TimelineItemVideoContent, + is TimelineItemStickerContent -> true + is TimelineItemAudioContent, + is TimelineItemRtcNotificationContent, + is TimelineItemEncryptedContent, + is TimelineItemFileContent, + TimelineItemLegacyCallInviteContent, + is TimelineItemLocationContent, + is TimelineItemPollContent, + TimelineItemRedactedContent, + is TimelineItemProfileChangeContent, + is TimelineItemRoomMembershipContent, + is TimelineItemStateEventContent, + is TimelineItemEmoteContent, + is TimelineItemNoticeContent, + is TimelineItemTextContent, + TimelineItemUnknownContent, + is TimelineItemVoiceContent -> false + } + } } is TimelineItem.Virtual -> false is TimelineItem.GroupedEvents -> false diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt index 169e5ba7f4..df31c205e9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionState.kt @@ -16,9 +16,13 @@ data class TimelineProtectionState( val protectionState: ProtectionState, val eventSink: (TimelineProtectionEvent) -> Unit, ) { - fun hideMediaContent(eventId: EventId?) = when (protectionState) { - is ProtectionState.RenderAll -> false - is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds + fun hideMediaContent(eventId: EventId?, isMine: Boolean = false) = if (isMine) { + false + } else { + when (protectionState) { + is ProtectionState.RenderAll -> false + is ProtectionState.RenderOnly -> eventId !in protectionState.eventIds + } } } 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 4d7242ebf5..4f023ad2bd 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 @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -42,6 +43,7 @@ import io.element.android.libraries.designsystem.components.avatar.anAvatarData 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.preview.ROOM_NAME 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.Text @@ -78,7 +80,8 @@ internal fun MessagesViewTopBar( Row( modifier = Modifier .clip(roundedCornerShape) - .clickable { onRoomDetailsClick() }, + .clickable { onRoomDetailsClick() } + .semantics { heading() }, horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -91,9 +94,12 @@ internal fun MessagesViewTopBar( modifier = titleModifier ) + val iconModifier = Modifier.size(16.dp) + when (dmUserIdentityState) { IdentityState.Verified -> { Icon( + modifier = iconModifier, imageVector = CompoundIcons.Verified(), tint = ElementTheme.colors.iconSuccessPrimary, contentDescription = null, @@ -101,6 +107,7 @@ internal fun MessagesViewTopBar( } IdentityState.VerificationViolation -> { Icon( + modifier = iconModifier, imageVector = CompoundIcons.ErrorSolid(), tint = ElementTheme.colors.iconCriticalPrimary, contentDescription = null, @@ -112,11 +119,13 @@ internal fun MessagesViewTopBar( when (sharedHistoryIcon) { SharedHistoryIcon.NONE -> Unit SharedHistoryIcon.SHARED -> Icon( + modifier = iconModifier, imageVector = CompoundIcons.History(), tint = ElementTheme.colors.iconInfoPrimary, contentDescription = stringResource(CommonStrings.common_shared_history), ) SharedHistoryIcon.WORLD_READABLE -> Icon( + modifier = iconModifier, imageVector = CompoundIcons.UserProfileSolid(), tint = ElementTheme.colors.iconInfoPrimary, contentDescription = stringResource(CommonStrings.common_world_readable_history), @@ -150,10 +159,7 @@ private fun RoomAvatarAndNameRow( ) Text( modifier = Modifier - .padding(horizontal = 8.dp) - .semantics { - heading() - }, + .padding(start = 8.dp), text = roomName ?: stringResource(CommonStrings.common_no_room_name), style = ElementTheme.typography.fontBodyLgMedium, fontStyle = FontStyle.Italic.takeIf { roomName == null }, @@ -168,9 +174,9 @@ private fun RoomAvatarAndNameRow( internal fun MessagesViewTopBarPreview() = ElementPreview { @Composable fun AMessagesViewTopBar( - roomName: String? = "Room name", + roomName: String? = ROOM_NAME, roomAvatar: AvatarData = anAvatarData( - name = "Room name", + name = ROOM_NAME, size = AvatarSize.TimelineRoom, ), isTombstoned: Boolean = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt index 2247566531..5ef4541f06 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/ThreadTopBar.kt @@ -17,8 +17,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -31,6 +32,7 @@ import io.element.android.libraries.designsystem.components.avatar.anAvatarData 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.preview.ROOM_NAME import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -57,7 +59,18 @@ internal fun ThreadTopBar( BackButton(onClick = onBackClick) }, title = { - Row(verticalAlignment = Alignment.CenterVertically) { + val name = roomName ?: stringResource(CommonStrings.common_no_room_name) + val description = stringResource( + CommonStrings.a11y_thread_in_room, + name, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clearAndSetSemantics { + heading() + contentDescription = description + }, + ) { Avatar( avatarData = roomAvatarData, avatarType = AvatarType.Room( @@ -68,17 +81,14 @@ internal fun ThreadTopBar( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp) - .semantics { - heading() - }, + .padding(horizontal = 8.dp), ) { Text( text = stringResource(CommonStrings.common_thread), style = ElementTheme.typography.fontBodyLgMedium, ) Text( - text = roomName ?: stringResource(CommonStrings.common_no_room_name), + text = name, style = ElementTheme.typography.fontBodySmRegular, fontStyle = FontStyle.Italic.takeIf { roomName == null }, color = ElementTheme.colors.textSecondary, @@ -96,9 +106,9 @@ internal fun ThreadTopBar( internal fun ThreadTopBarPreview() = ElementPreview { @Composable fun AThreadTopBar( - roomName: String? = "Room name", + roomName: String? = ROOM_NAME, roomAvatarData: AvatarData = anAvatarData( - name = "Room name", + name = ROOM_NAME, size = AvatarSize.TimelineRoom, ), isTombstoned: Boolean = false, @@ -123,7 +133,7 @@ internal fun ThreadTopBarPreview() = ElementPreview { HorizontalDivider() AThreadTopBar( roomAvatarData = anAvatarData( - name = "Room name", + name = ROOM_NAME, url = "https://some-avatar.jpg", size = AvatarSize.TimelineRoom, ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt index 0506026b86..e298b3af26 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt @@ -9,6 +9,11 @@ package io.element.android.features.messages.impl.typing import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CHARLIE +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID +import io.element.android.libraries.designsystem.preview.USER_NAME_EVE import kotlinx.collections.immutable.toImmutableList class TypingNotificationStateProvider : PreviewParameterProvider { @@ -22,7 +27,7 @@ class TypingNotificationStateProvider : PreviewParameterProvider context.getString(CommonStrings.common_file) is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) is TimelineItemLegacyCallInviteContent -> context.getString(CommonStrings.common_unsupported_call) - is TimelineItemRtcNotificationContent -> context.getString(CommonStrings.common_call_started) + is TimelineItemRtcNotificationContent -> when (content.state) { + is RtcNotificationState.Declined -> { + if (content.state.byMe) { + context.getString(CommonStrings.common_call_you_declined) + } else { + context.getString(CommonStrings.common_call_declined) + } + } + RtcNotificationState.Started -> context.getString(CommonStrings.common_call_started) + } } // Truncate the message to a safe length to avoid crashes in Compose .toSafeLength() diff --git a/features/messages/impl/src/main/res/values-be/translations.xml b/features/messages/impl/src/main/res/values-be/translations.xml index 80a5df8d6b..8b40c72040 100644 --- a/features/messages/impl/src/main/res/values-be/translations.xml +++ b/features/messages/impl/src/main/res/values-be/translations.xml @@ -16,6 +16,8 @@ "Сімвалы" "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз." + "Апрацоўка…" + "Дадаць медыя" "Заблакіраваць карыстальніка" "Адзначце, ці хочаце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка" "Гэтае паведамленне будзе перададзена адміністратару вашага хатняга сервера. Яны не змогуць прачытаць зашыфраваныя паведамленні." @@ -25,7 +27,7 @@ "Запісаць відэа" "Далучэнне" "Бібліятэка фота & відэа" - "Месцазнаходжанне" + "Абагуліць месцазнаходжанне" "Апытанне" "Фармаціраванне тэксту" "Гісторыя паведамленняў зараз недаступна." diff --git a/features/messages/impl/src/main/res/values-ca/translations.xml b/features/messages/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..f9b2685c7d --- /dev/null +++ b/features/messages/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,68 @@ + + + "El remitent de l\'esdeveniment no coincideix amb el propietari del dispositiu que l\'ha enviat." + "L\'autenticitat d\'aquest missatge xifrat en aquest dispositiu no es pot garantir." + "Xifrat per un usuari prèviament verificat." + "No xifrat." + "Xifrat per un dispositiu desconegut o eliminat." + "Xifrat per un dispositiu no verificat pel seu propietari." + "Xifrat per un usuari no verificat." + "Activitats" + "Banderes" + "Menjar & begudes" + "Animals i naturalesa" + "Objectes" + "Cares i persones" + "Viatges i llocs" + "Emoticones recents" + "Símbols" + "És possible que les llegendes no siguin visibles pels que utilitzin aplicacions antigues." + "No s\'ha pogut processar el contingut que s\'havia de pujar. Torna-ho a provar." + "No s\'ha pogut pujar el contingut. Torna-ho a provar." + "Bloqueja usuari" + "Marca si vols ocultar tots els missatges actuals i futurs d\'aquest usuari" + "Aquest missatge s\'enviarà a l\'administrador del teu servidor. No podrà llegir cap missatge xifrat." + "Motiu de la denúncia del contingut" + "Càmera" + "Fes una foto" + "Enregistra vídeo" + "Fitxer adjunt" + "Galeria de fotos i vídeos" + "Ubicació" + "Votació" + "Format de text" + "L\'historial de missatges no està disponible actualment." + "L\'historial de missatges no està disponible en aquesta sala. Verifica aquest dispositiu per veure l\'historial de missatges." + "Vols tornar-los a convidar-los?" + "No hi ha ningú més al xat" + "Notifica tota la sala" + "Tothom" + "Torna a enviar" + "No s\'han pogut enviar els missatges" + "Afegeix reacció" + "Aquest és el principi de %1$s." + "Principi d\'aquesta conversa." + "Trucada no compatible. Comprova si la persona que truca està utilitzant la nova aplicació Element X." + "Mostra\'n menys" + "Missatge copiat" + "No tens permís per enviar res en aquesta sala" + "Mostra\'n menys" + "Mostra\'n més" + "Nous" + + "%1$d canvi a la sala" + "%1$d canvis a la sala" + + "Aquesta sala ha estat substituïda i ja no està activa" + "Mostra els missatges antics" + "Aquesta sala és la continuació d\'una altra" + + "%1$s, %2$s i %3$d més" + "%1$s, %2$s i %3$d més" + + + "%1$s està escrivint" + "%1$s estan escrivint" + + "%1$s i %2$s" + diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 93ea0d8178..9bf1c5c079 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -1,7 +1,7 @@ "Odesílatel události se neshoduje s vlastníkem zařízení, které ji odeslalo." - "Autenticitu této zašifrované zprávy nelze na tomto zařízení zaručit." + "Pravost této šifrované zprávy nelze na tomto zařízení zaručit." "Zašifrováno dříve ověřeným uživatelem." "Není zašifrováno." "Šifrováno neznámým nebo smazaným zařízením." diff --git a/features/messages/impl/src/main/res/values-da/translations.xml b/features/messages/impl/src/main/res/values-da/translations.xml index 390d03f451..e068df293a 100644 --- a/features/messages/impl/src/main/res/values-da/translations.xml +++ b/features/messages/impl/src/main/res/values-da/translations.xml @@ -35,7 +35,7 @@ "Optag video" "Vedhæftning" "Foto- og videobibliotek" - "Del lokation" + "Del placering" "Afstemning" "Tekstformatering" "Beskedhistorikken er i øjeblikket ikke tilgængelig." diff --git a/features/messages/impl/src/main/res/values-et/translations.xml b/features/messages/impl/src/main/res/values-et/translations.xml index 8da4d23f79..86b1995f32 100644 --- a/features/messages/impl/src/main/res/values-et/translations.xml +++ b/features/messages/impl/src/main/res/values-et/translations.xml @@ -16,6 +16,18 @@ "Reisimine ja kohad" "Hiljutised emojid" "Sümbolid" + "Pööra pilti ümber rõhttelje" + "Pilt on pööratud ümber rõhttelje" + "Pildi algne variant" + "Pööra pilti ümber püsttelje" + "Pilt on pööratud ümber püsttelje" + "Pildi algne variant" + "Pööra pilti vasakule" + + "%1$d kraad" + "%1$d kraadi" + + "Muuda fotot" "Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele." "Klõpsa üleslaaditava video kvaliteedi muutmiseks" "Faili üleslaadimine ei õnnestunud." @@ -26,6 +38,7 @@ "Objekt %1$d/%2$d" "Optimeeri pildikvaliteeti" "Töötlen…" + "Lisa meediumi" "Blokeeri kasutaja" "Vali see eelistus, kui sa soovid peita selle kasutaja kõik senised ja tulevased sõnumid" "Teade selle sõnumi kohta edastatakse sinu koduserveri haldajale. Haldajal ei ole võimalik lugeda krüptitud sõnumite sisu." @@ -35,7 +48,7 @@ "Salvesta video" "Manus" "Fotode ja videote galerii" - "Asukoht" + "Jaga asukohta" "Küsitlus" "Tekstivorming" "Sõnumite ajalugu pole hetkel saadaval" diff --git a/features/messages/impl/src/main/res/values-fa/translations.xml b/features/messages/impl/src/main/res/values-fa/translations.xml index 446239c095..19f55a5559 100644 --- a/features/messages/impl/src/main/res/values-fa/translations.xml +++ b/features/messages/impl/src/main/res/values-fa/translations.xml @@ -27,7 +27,7 @@ "ضبط ویدیو" "پیوست" "کتابخانهٔ عکس و ویدیو" - "مکان" + "هم‌رسانی مکان" "نظرسنجی" "قالب‌بندی متن" "تاریخچه پیام درحال حاضر دردسترس نیست." @@ -56,5 +56,13 @@ "این اتاق جایگزین شده و دیگر فعّال نیست" "دیدن پیام‌های قدیمی" "این اتاق ادامهٔ اتاقی دیگر است" + + "%1$s، %2$s و %3$d سایر" + "%1$s، %2$s و %3$d موارد دیگر" + + + "%1$s در حال تایپ است" + "%1$s در حال تایپ هستند" + "%1$s و %2$s" diff --git a/features/messages/impl/src/main/res/values-fi/translations.xml b/features/messages/impl/src/main/res/values-fi/translations.xml index 33e8f64d57..881a5386e7 100644 --- a/features/messages/impl/src/main/res/values-fi/translations.xml +++ b/features/messages/impl/src/main/res/values-fi/translations.xml @@ -16,6 +16,18 @@ "Matkustaminen ja paikat" "Viimeaikaiset emojit" "Symbolit" + "Käännä kuva vaakasuunnassa" + "Käännetty vaakasuunnassa" + "Alkuperäinen" + "Käännä kuva pystysuunnassa" + "Käännetty pystysuunnassa" + "Alkuperäinen" + "Kierrä kuvaa vasemmalle" + + "%1$d aste" + "%1$d astetta" + + "Muokkaa kuvaa" "Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia." "Napauta muuttaaksesi videon lähetyslaatua" "Tiedostoa ei voitu lähettää." @@ -26,6 +38,7 @@ "Kohde %1$d / %2$d" "Optimoi kuvanlaatu" "Käsitellään…" + "Lisää mediaa" "Estä käyttäjä" "Valitse tämä, jos haluat piilottaa kaikki nykyiset ja tulevat viestit tältä käyttäjältä" "Tämä viesti ilmoitetaan kotipalvelimesi ylläpitäjälle. Ylläpitäjä ei pysty lukemaan salattuja viestejä." diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index 0d8dab284f..3505fd520d 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -16,6 +16,18 @@ "Voyages & lieux" "Emojis récents" "Symboles" + "Retourner l’image horizontalement" + "Retournée horizontalement" + "Originale" + "Retourner l’image verticalement" + "Retournée verticalement" + "Originale" + "Pivoter l’image vers la gauche" + + "%1$d degré" + "%1$d degrés" + + "Modifier la photo" "Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications." "Cliquez pour modifier la qualité d’envoi de la vidéo" "Le fichier n’a pas pu être envoyé." @@ -26,6 +38,7 @@ "Élément %1$d sur %2$d" "Optimiser la qualité de l’image" "Traitement en cours…" + "Ajouter un média" "Bloquer l’utilisateur" "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur." "Ce message sera signalé à l’administrateur de votre serveur d’accueil. Il ne pourra lire aucun message chiffré." diff --git a/features/messages/impl/src/main/res/values-hu/translations.xml b/features/messages/impl/src/main/res/values-hu/translations.xml index 3cbd86e8d1..bb33520cb6 100644 --- a/features/messages/impl/src/main/res/values-hu/translations.xml +++ b/features/messages/impl/src/main/res/values-hu/translations.xml @@ -16,6 +16,18 @@ "Utazás és helyek" "Legutóbbi emodzsik" "Szimbólumok" + "Kép vízszintes tükrözése" + "Vízszintesen tükrözve" + "Eredeti" + "Kép függőleges tükrözése" + "Függőlegesen tükrözve" + "Eredeti" + "Kép elforgatása balra" + + "%1$d fok" + "%1$d fok" + + "Fotó szerkesztése" "Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára." "Koppintson a feltöltött videók minőségének módosításához" "A fájl nem tölthető fel." @@ -26,6 +38,7 @@ "%1$d. elem / %2$d" "Képminőség optimalizációja" "Feldolgozás…" + "Média hozzáadása" "Felhasználó letiltása" "Jelölje be, ha el akarja rejteni az összes jelenlegi és jövőbeli üzenetet ettől a felhasználótól" "Ez az üzenet jelentve lesz a Matrix-kiszolgáló adminisztrátorának. Nem fogja tudni elolvasni a titkosított üzeneteket." diff --git a/features/messages/impl/src/main/res/values-in/translations.xml b/features/messages/impl/src/main/res/values-in/translations.xml index 508f4d5476..7a0994f65e 100644 --- a/features/messages/impl/src/main/res/values-in/translations.xml +++ b/features/messages/impl/src/main/res/values-in/translations.xml @@ -34,7 +34,7 @@ "Rekam video" "Lampiran" "Pustaka Foto & Video" - "Lokasi" + "Membagi Lokasi" "Jajak pendapat" "Pemformatan Teks" "Riwayat pesan saat ini tidak tersedia di ruangan ini" diff --git a/features/messages/impl/src/main/res/values-ja/translations.xml b/features/messages/impl/src/main/res/values-ja/translations.xml index 4fd18e8db5..455996fd1a 100644 --- a/features/messages/impl/src/main/res/values-ja/translations.xml +++ b/features/messages/impl/src/main/res/values-ja/translations.xml @@ -16,6 +16,11 @@ "旅・場所" "最近使用" "記号" + "画像を左に回転" + + "%1$d°" + + "写真を編集" "古いアプリケーションを使用しているユーザーはキャプションを見られない可能性があります。" "動画のアップロード画質を変更するにはタップしてください" "ファイルをアップロードに失敗しました。" @@ -26,6 +31,7 @@ "個数 %1$d / %2$d" "画像の品質を最適化" "処理中…" + "メディアを追加" "ユーザーをブロック" "このユーザーからのメッセージをすべて非表示にする場合はチェックしてください。" "このメッセージはホームサーバーの管理者に報告されます。暗号化されたメッセージを確認することはできません。" @@ -33,8 +39,8 @@ "カメラ" "写真を撮影" "動画を撮影" - "添付ファイル" - "アルバムの写真・動画" + "ファイルを添付" + "アルバムの写真と動画" "場所を共有" "投票" "書式設定" diff --git a/features/messages/impl/src/main/res/values-ko/translations.xml b/features/messages/impl/src/main/res/values-ko/translations.xml index daede9ec1d..fbcb0cab52 100644 --- a/features/messages/impl/src/main/res/values-ko/translations.xml +++ b/features/messages/impl/src/main/res/values-ko/translations.xml @@ -16,6 +16,11 @@ "여행 & 장소" "최근 이모지" "상징" + "이미지를 왼쪽으로 회전" + + "%1$d도" + + "사진 편집" "캡션은 오래된 앱을 사용하는 사용자에게 표시되지 않을 수 있습니다." "비디오 업로드 품질을 변경하려면 탭하세요" "파일을 업로드할 수 없습니다." @@ -26,6 +31,7 @@ "전체 %2$d개 중 %1$d번째 파일" "이미지 품질 최적화" "처리 중…" + "미디어 추가" "사용자 차단하기" "이 사용자의 현재 및 향후 모든 메시지를 숨기려면 확인하세요." "이 메시지는 홈서버의 관리자에게 보고되었습니다. 암호화된 메시지는 읽을 수 없습니다." diff --git a/features/messages/impl/src/main/res/values-pl/translations.xml b/features/messages/impl/src/main/res/values-pl/translations.xml index 18a8af6cb0..d8ffaac54c 100644 --- a/features/messages/impl/src/main/res/values-pl/translations.xml +++ b/features/messages/impl/src/main/res/values-pl/translations.xml @@ -16,6 +16,19 @@ "Podróż i miejsca" "Ostatnie emoji" "Symbole" + "Odwróć obraz w poziomie" + "Odwrócono w poziomie" + "Oryginalny" + "Odwróć obraz w pionie" + "Odwrócono w pionie" + "Oryginalny" + "Obróć obraz w lewo" + + "%1$d stopień" + "%1$d stopnie" + "%1$d stopni" + + "Edytuj zdjęcie" "Opis może być niedostępny dla osób korzystających ze starszej wersji aplikacji." "Dotknij, aby zmienić jakość przesyłania wideo." "Nie udało się przesłać pliku." @@ -26,16 +39,17 @@ "Pozycja %1$d z %2$d" "Zoptymalizuj jakość obrazu" "Przetwarzanie…" + "Dodaj media" "Zablokuj użytkownika" "Sprawdź, czy chcesz ukryć wszystkie bieżące i przyszłe wiadomości od tego użytkownika." "Ta wiadomość zostanie zgłoszona do administratora Twojego serwera domowego. Nie będzie mógł on przeczytać żadnych zaszyfrowanych wiadomości." "Powód zgłoszenia treści" "Kamera" "Zrób zdjęcie" - "Nagraj film" + "Nagraj wideo" "Załącznik" "Zdjęcia i filmy" - "Lokalizacja" + "Udostępnij lokalizację" "Ankieta" "Formatowanie tekstu" "Historia wiadomości jest obecnie niedostępna." diff --git a/features/messages/impl/src/main/res/values-pt/translations.xml b/features/messages/impl/src/main/res/values-pt/translations.xml index 054d2a1759..2fb5173169 100644 --- a/features/messages/impl/src/main/res/values-pt/translations.xml +++ b/features/messages/impl/src/main/res/values-pt/translations.xml @@ -35,7 +35,7 @@ "Gravar vídeo" "Anexo" "Biblioteca de fotos e vídeos" - "Localização" + "Partilhar localização" "Sondagem" "Formatação de texto" "De momento, o histórico de mensagens está indisponível." diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index a7d22b6542..3744beff8f 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -16,16 +16,30 @@ "Путешествия и места" "Недавние эмодзи" "Символы" + "Перевернуть изображение по горизонтали" + "Перевернуто по горизонтали" + "Оригинал" + "Перевернуть изображение по вертикали" + "Перевернуто по вертикали" + "Оригинал" + "Повернуть изображение влево." + + "%1$d градус" + "%1$d градуса" + "%1$d градусов" + + "Редактировать фото" "Подпись может быть не видна пользователям старых приложений." "Нажмите, чтобы изменить качество загружаемого видео." "Файл не может быть загружен." - "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." - "Не удалось загрузить медиафайлы, попробуйте еще раз." + "Не удалось обработать медиафайл для загрузки, попробуйте ещё раз." + "Не удалось загрузить медиафайлы, попробуйте ещё раз." "Максимальный размер файла: %1$s." "Файл слишком большой для загрузки." "%1$d из %2$d" "Оптимизировать качество изображения" "Обработка…" + "Добавить медиа" "Заблокировать пользователя" "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя" "Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения." diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index 4f267d8552..d2022c4882 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -35,7 +35,7 @@ "Nahrať video" "Príloha" "Knižnica fotografií a videí" - "Poloha" + "Zdieľať polohu" "Anketa" "Formátovanie textu" "História správ v tejto miestnosti nie je momentálne k dispozícii" diff --git a/features/messages/impl/src/main/res/values-uk/translations.xml b/features/messages/impl/src/main/res/values-uk/translations.xml index c51744a408..6bc9f67011 100644 --- a/features/messages/impl/src/main/res/values-uk/translations.xml +++ b/features/messages/impl/src/main/res/values-uk/translations.xml @@ -16,6 +16,13 @@ "Подорожі та місця" "Нещодавні емодзі" "Символи" + "Повернути зображення вліво" + + "%1$d градус" + "%1$d градуси" + "%1$d градусів" + + "Редагувати фото" "Користувачі старих застосунків можуть не бачити підписи." "Натисніть, щоб змінити якість вивантажуваного відео" "Файл не може бути вивантажено." @@ -26,6 +33,7 @@ "Елемент %1$d з %2$d" "Оптимізувати якість зображення" "Обробка…" + "Додати медіа" "Заблокувати користувача" "Перевірте, чи хочете ви приховати всі поточні та майбутні повідомлення від цього користувача" "Це повідомлення буде надіслано адміністраторам вашого домашнього сервера. Вони не зможуть прочитати зашифровані повідомлення." @@ -35,7 +43,7 @@ "Записати відео" "Вкладення" "Бібліотека фото та відео" - "Розташування" + "Поділитися місцеперебуванням" "Опитування" "Форматування тексту" "Історія повідомлень наразі недоступна." 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 26518d656c..e2300912ae 100644 --- a/features/messages/impl/src/main/res/values-zh/translations.xml +++ b/features/messages/impl/src/main/res/values-zh/translations.xml @@ -16,18 +16,30 @@ "文旅景点" "最近的 Emoji" "符号" + "水平翻转图像" + "水平翻转" + "原始" + "垂直翻转图像" + "垂直翻转" + "原始" + "向左旋转图像" + + "%1$d 度" + + "编辑照片" "使用旧版应用程序的用户可能无法看到字幕。" "点按以更改视频上传质量" "无法上传该文件。" "处理要上传的媒体失败,请重试。" "上传媒体失败,请重试。" - "允许的最大文件大小为%1$s 。" + "允许的最大文件大小为 %1$s。" "文件太大,无法上传" "第 %1$d 个项目,共 %2$d 个" "优化图像质量" "处理中…" + "添加媒体" "屏蔽用户" - "请确认是否要隐藏该用户当前和未来的所有信息" + "请确认是否要隐藏该用户当前和未来的所有消息" "此消息将举报给服务器管理员。他们无法读取任何加密消息。" "举报此内容的理由" "相机" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index f5629f5e2d..173a4e78ad 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -16,6 +16,18 @@ "Travel & Places" "Recent emojis" "Symbols" + "Flip image horizontally" + "Flipped horizontally" + "Original" + "Flip image vertically" + "Flipped vertically" + "Original" + "Rotate the image to the left" + + "%1$d degree" + "%1$d degrees" + + "Edit photo" "Captions might not be visible to people using older apps." "Tap to change the video upload quality" "The file could not be uploaded." @@ -26,6 +38,7 @@ "Item %1$d of %2$d" "Optimise image quality" "Processing…" + "Add media" "Block user" "Check if you want to hide all current and future messages from this user" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." 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 dc50fca2c3..9679e23a23 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 @@ -23,6 +23,7 @@ import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.impl.pinned.banner.createPinnedEventsTimelineProvider import io.element.android.features.messages.impl.timeline.createTimelineController import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint +import io.element.android.libraries.androidutils.system.DeviceHasVulkanSupport import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -42,6 +43,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.testCoroutineDispatchers +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -85,6 +87,7 @@ class DefaultMessagesEntryPointTest { knockRequestsListEntryPoint = FakeKnockRequestsListEntryPoint(), dateFormatter = FakeDateFormatter(), coroutineDispatchers = testCoroutineDispatchers(), + hasVulkanSupport = DeviceHasVulkanSupport(mockk(relaxed = true)) ) } val callback = object : MessagesEntryPoint.Callback { 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 44d82f1a7c..68f9a8d17a 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 @@ -28,6 +28,7 @@ class FakeMessagesNavigator( private val navigateToDeveloperSettingsLambda: () -> Unit = { lambdaError() }, private val onOpenThreadLambda: (threadRootId: ThreadId, focusedEventId: EventId?) -> Unit = { _, _ -> lambdaError() }, private val closeLambda: () -> Unit = { lambdaError() }, + private val navigateToCurrentLiveLocationLambda: () -> Unit = { lambdaError() }, ) : MessagesNavigator { override fun navigateToEventDebugInfo(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { onShowEventDebugInfoClickLambda(eventId, debugInfo) @@ -65,6 +66,10 @@ class FakeMessagesNavigator( navigateToDeveloperSettingsLambda() } + override fun navigateToCurrentLiveLocation() { + navigateToCurrentLiveLocationLambda() + } + 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 50bacb005f..65aa1d857e 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 @@ -13,6 +13,7 @@ package io.element.android.features.messages.impl import androidx.lifecycle.Lifecycle import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.PinUnpinAction +import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState @@ -120,6 +121,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds @Suppress("LargeClass") class MessagesPresenterTest { @@ -140,6 +142,39 @@ class MessagesPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.inviteProgress).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.showReinvitePrompt).isFalse() + assertThat(initialState.showLiveLocationShareBanner).isFalse() + } + } + + @Test + fun `present - exposes live location sharing banner visibility for current room`() = runTest { + val liveLocationShareManager = FakeActiveLiveLocationShareManager( + startShareLambda = { _, _ -> Result.success(Unit) }, + ) + liveLocationShareManager.startShare(A_ROOM_ID, 60.seconds) + val presenter = createMessagesPresenter(liveLocationShareManager = liveLocationShareManager) + + presenter.testWithLifecycleOwner { + val state = consumeItemsUntilTimeout().last() + assertThat(state.showLiveLocationShareBanner).isTrue() + } + } + + @Test + fun `present - stop live location share delegates to manager for current room`() = runTest { + val stopShareLambda = lambdaRecorder> { Result.success(Unit) } + val liveLocationShareManager = FakeActiveLiveLocationShareManager( + stopShareLambda = stopShareLambda + ) + val presenter = createMessagesPresenter(liveLocationShareManager = liveLocationShareManager) + + presenter.testWithLifecycleOwner { + val state = consumeItemsUntilTimeout().last() + state.eventSink(MessagesEvent.StopLiveLocationShare) + advanceUntilIdle() + assert(stopShareLambda) + .isCalledOnce() + .with(value(A_ROOM_ID)) } } @@ -563,7 +598,7 @@ class MessagesPresenterTest { baseRoom = FakeBaseRoom( roomPermissions = roomPermissions(), ).apply { - givenRoomInfo(aRoomInfo(isDirect = true, joinedMembersCount = 1, activeMembersCount = 1)) + givenRoomInfo(aRoomInfo(isDm = true, joinedMembersCount = 1, activeMembersCount = 1)) }, typingNoticeResult = { Result.success(Unit) }, ) @@ -1077,7 +1112,7 @@ class MessagesPresenterTest { canRedactOwn = true, canPinUnpin = true, ), - initialRoomInfo = aRoomInfo(isDirect = true, isEncrypted = true) + initialRoomInfo = aRoomInfo(isDm = true, isEncrypted = true) ).apply { givenRoomMembersState(RoomMembersState.Ready(persistentListOf(aRoomMember(userId = A_SESSION_ID), aRoomMember(userId = A_USER_ID_2)))) }, @@ -1347,6 +1382,7 @@ class MessagesPresenterTest { actionListEventSink: (ActionListEvent) -> Unit = {}, addRecentEmoji: AddRecentEmoji = AddRecentEmoji { _ -> lambdaError() }, markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead(), + liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ): MessagesPresenter { return MessagesPresenter( navigator = navigator, @@ -1376,6 +1412,7 @@ class MessagesPresenterTest { featureFlagService = featureFlagService, addRecentEmoji = addRecentEmoji, markAsFullyRead = markAsFullyRead, + liveLocationShareManager = liveLocationShareManager, sessionCoroutineScope = backgroundScope, ) } 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 62b9eac68d..30106cbaf3 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 @@ -6,13 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithTag @@ -25,8 +27,8 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeRight +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.compose.ui.text.AnnotatedString -import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseStore @@ -75,85 +77,80 @@ 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.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config import kotlin.time.Duration.Companion.milliseconds -@RunWith(AndroidJUnit4::class) -class MessagesViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class MessagesViewTest : RobolectricTest() { @Test - fun `clicking on back invoke expected callback`() { + fun `clicking on back invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on room name invoke expected callback`() { + fun `clicking on room name invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onRoomDetailsClick = callback, ) - rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() + onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick() } } @Test - fun `clicking on join call invoke expected callback`() { + fun `clicking on join call invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder ) ensureCalledOnceWithParam(false) { callback -> - rule.setMessagesView( + setMessagesView( state = state, onJoinCallClick = callback, ) - val joinCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_call) - rule.onNodeWithContentDescription(joinCallContentDescription).performClick() + val joinCallContentDescription = activity!!.getString(CommonStrings.a11y_start_call) + onNodeWithContentDescription(joinCallContentDescription).performClick() } } @Test - fun `clicking on join voice call invoke expected callback`() { + fun `clicking on join voice call invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( eventSink = eventsRecorder, roomCallState = aStandByCallState(isDM = true) ) ensureCalledOnceWithParam(true) { callback -> - rule.setMessagesView( + setMessagesView( state = state, onJoinCallClick = callback, ) - val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call) - rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() + val joinVoiceCallContentDescription = activity!!.getString(CommonStrings.a11y_start_voice_call) + onNodeWithContentDescription(joinVoiceCallContentDescription).performClick() } } @Test - fun `clicking on an Event invoke expected callback`() { + fun `clicking on an Event invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( timelineState = aTimelineState( @@ -167,12 +164,12 @@ class MessagesViewTest { expectedParam2 = timelineItem, result = true, ) - rule.setMessagesView( + setMessagesView( state = state, onEventClick = callback, ) // Cannot perform click on "Text", it's not detected. Use tag instead - rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() + onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() callback.assertSuccess() } @@ -202,7 +199,7 @@ class MessagesViewTest { userHasPermissionToRedactOther: Boolean = false, userHasPermissionToSendReaction: Boolean = false, userCanPinEvent: Boolean = false, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( actionListState = anActionListState( @@ -220,11 +217,11 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) // Cannot perform click on "Text", it's not detected. Use tag instead - rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } + onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() } eventsRecorder.assertSingle( ActionListEvent.ComputeForMessage( event = timelineItem, @@ -235,11 +232,10 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on a read receipt list emits the expected Event`() { + fun `clicking on a read receipt list emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( - renderReadReceipts = true, timelineItems = persistentListOf( aTimelineItemEvent( readReceiptState = aTimelineItemReadReceipts( @@ -255,10 +251,10 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() + onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle(ReadReceiptBottomSheetEvent.EventSelected(timelineItem)) } @@ -272,7 +268,7 @@ class MessagesViewTest { swipeTest(userHasPermissionToSendMessage = false) } - private fun swipeTest(userHasPermissionToSendMessage: Boolean) { + private fun swipeTest(userHasPermissionToSendMessage: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true) val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false) @@ -285,10 +281,10 @@ class MessagesViewTest { ), eventSink = eventsRecorder, ) - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onAllNodesWithTag(TestTags.messageBubble.value).apply { + onAllNodesWithTag(TestTags.messageBubble.value).apply { onFirst().performTouchInput { swipeRight(endX = 200f) } onLast().performTouchInput { swipeRight(endX = 200f) } } @@ -300,7 +296,7 @@ class MessagesViewTest { } @Test - fun `clicking on send location invoke expected callback`() { + fun `clicking on send location invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( composerState = aMessageComposerState( @@ -309,16 +305,16 @@ class MessagesViewTest { eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onSendLocationClick = callback, ) - rule.clickOn(R.string.screen_room_attachment_source_location) + clickOn(R.string.screen_room_attachment_source_location) } } @Test - fun `clicking on create poll invoke expected callback`() { + fun `clicking on create poll invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( composerState = aMessageComposerState( @@ -327,25 +323,25 @@ class MessagesViewTest { eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMessagesView( + setMessagesView( state = state, onCreatePollClick = callback, ) // Then click on the poll action - rule.clickOn(R.string.screen_room_attachment_source_poll) + clickOn(R.string.screen_room_attachment_source_poll) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on the avatar of the sender of an Event emits the expected event`() { + fun `clicking on the avatar of the sender of an Event emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder ) val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() - rule.setMessagesView(state = state) - rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + setMessagesView(state = state) + onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle( MessagesEvent.OnUserClicked( MatrixUser( @@ -359,12 +355,12 @@ class MessagesViewTest { @Test @Config(qualifiers = "h1024dp") - fun `clicking on the display name of the sender of an Event emits expected event`() { + fun `clicking on the display name of the sender of an Event emits expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState(eventSink = eventsRecorder) val timelineEvent = state.timelineState.timelineItems.filterIsInstance().first() - rule.setMessagesView(state = state) - rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() + setMessagesView(state = state) + onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick() eventsRecorder.assertSingle( MessagesEvent.OnUserClicked( MatrixUser( @@ -377,7 +373,7 @@ class MessagesViewTest { } @Test - fun `selecting a action on a message emits the expected Event`() { + fun `selecting a action on a message emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( eventSink = eventsRecorder @@ -395,17 +391,17 @@ class MessagesViewTest { ) ), ) - rule.setMessagesView( + setMessagesView( state = stateWithMessageAction, ) - rule.clickOn(CommonStrings.action_edit) + clickOn(CommonStrings.action_edit) // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Edit, timelineItem)) } @Test - fun `clicking on a reaction emits the expected Event`() { + fun `clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -414,10 +410,10 @@ class MessagesViewTest { eventSink = eventsRecorder, ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onAllNodesWithText( + onAllNodesWithText( text = "👍️", useUnmergedTree = true, ).onFirst().performClick() @@ -425,7 +421,7 @@ class MessagesViewTest { } @Test - fun `long clicking on a reaction emits the expected Event`() { + fun `long clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -437,10 +433,10 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - rule.onAllNodesWithText( + onAllNodesWithText( text = "👍️", useUnmergedTree = true, ).onFirst().performTouchInput { longClick() } @@ -448,7 +444,7 @@ class MessagesViewTest { } @Test - fun `clicking on more reaction emits the expected Event`() { + fun `clicking on more reaction emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -459,16 +455,16 @@ class MessagesViewTest { ), ) val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event - rule.setMessagesView( + setMessagesView( state = state, ) - val moreReactionContentDescription = rule.activity.getString(R.string.screen_room_timeline_add_reaction) - rule.onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() + val moreReactionContentDescription = activity!!.getString(R.string.screen_room_timeline_add_reaction) + onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick() eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) } @Test - fun `clicking on more reaction from action list emits the expected Event`() { + fun `clicking on more reaction from action list emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState( @@ -491,18 +487,18 @@ class MessagesViewTest { eventSink = eventsRecorder ), ) - rule.setMessagesView( + setMessagesView( state = stateWithActionListState, ) - val moreReactionContentDescription = rule.activity.getString(CommonStrings.a11y_react_with_other_emojis) - rule.onNodeWithContentDescription(moreReactionContentDescription).performClick() + val moreReactionContentDescription = activity!!.getString(CommonStrings.a11y_react_with_other_emojis) + onNodeWithContentDescription(moreReactionContentDescription).performClick() // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem)) } @Test - fun `clicking on verified user send failure from action list emits the expected Event`() { + fun `clicking on verified user send failure from action list emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState() val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event @@ -519,21 +515,21 @@ class MessagesViewTest { ), timelineState = aTimelineState(eventSink = eventsRecorder) ) - rule.setMessagesView( + 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() + val verifiedUserSendFailure = activity!!.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice") + onNodeWithText(verifiedUserSendFailure).performClick() // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem)) } @Test - fun `clicking on a custom emoji emits the expected Events`() { + fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest { val aUnicode = "🙈" val customReactionStateEventsRecorder = EventsRecorder() val eventsRecorder = EventsRecorder() @@ -563,18 +559,18 @@ class MessagesViewTest { eventSink = customReactionStateEventsRecorder ), ) - rule.setMessagesView( + setMessagesView( state = stateWithCustomReactionState, ) - rule.onNodeWithText(aUnicode, useUnmergedTree = true).performClick() + onNodeWithText(aUnicode, useUnmergedTree = true).performClick() // Give time for the close animation to complete - rule.mainClock.advanceTimeBy(milliseconds = 1_000) + mainClock.advanceTimeBy(milliseconds = 1_000) customReactionStateEventsRecorder.assertSingle(CustomReactionEvent.DismissCustomReactionSheet) eventsRecorder.assertSingle(MessagesEvent.ToggleReaction(aUnicode, timelineItem.eventOrTransactionId)) } @Test - fun `clicking on pinned messages banner emits the expected Event`() { + fun `clicking on pinned messages banner emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMessagesState( timelineState = aTimelineState(eventSink = eventsRecorder), @@ -587,16 +583,16 @@ class MessagesViewTest { ), ), ) - rule.setMessagesView(state = state) + setMessagesView(state = state) // Clear initial 'LoadMore' event emitted when setting the state eventsRecorder.clear() - rule.onNodeWithText("This is a pinned message").performClick() + onNodeWithText("This is a pinned message").performClick() eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) } @Test - fun `clicking on successor room button emits expected event`() { + fun `clicking on successor room button emits expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val successorRoomId = RoomId("!successor:server.org") val state = aMessagesState( @@ -606,18 +602,18 @@ class MessagesViewTest { ), timelineState = aTimelineState(eventSink = eventsRecorder) ) - rule.setMessagesView(state = state) + 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) + val text = 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() + onAllNodesWithText(text).onFirst().performClick() eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId)) } @Test - fun `clicking on threads list button calls the expected function`() { + fun `clicking on threads list button calls the expected function`() = runAndroidComposeUiTest { val state = aMessagesState( threads = MessagesState.Threads( hasThreads = true, @@ -625,28 +621,66 @@ class MessagesViewTest { ) ) val onThreadsListClicked = lambdaRecorder {} - rule.setMessagesView( + setMessagesView( state = state, onThreadsListClicked = onThreadsListClicked, ) - rule.onNodeWithContentDescription("Threads").performClick() + onNodeWithContentDescription("Threads").performClick() onThreadsListClicked.assertions().isCalledOnce() } @Test - fun `no banner shown when there is no successor room`() { + fun `no banner shown when there is no successor room`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aMessagesState( successorRoom = null, eventSink = eventsRecorder ) - rule.setMessagesView(state = state) - rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) - rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) + setMessagesView(state = state) + assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message) + assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action) + } + + @Test + fun `live location banner is visible when current room is sharing`() = runAndroidComposeUiTest { + val state = aMessagesState(isCurrentlySharingLiveLocationInRoom = true) + setMessagesView(state = state) + onNodeWithText(activity!!.getString(CommonStrings.screen_room_live_location_banner)).assertExists() + } + + @Test + fun `live location banner is hidden when current room is not sharing`() = runAndroidComposeUiTest { + val state = aMessagesState(isCurrentlySharingLiveLocationInRoom = false) + setMessagesView(state = state) + onNodeWithText(activity!!.getString(CommonStrings.screen_room_live_location_banner)).assertDoesNotExist() + } + + @Test + fun `clicking stop on live location banner emits expected event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val state = aMessagesState( + isCurrentlySharingLiveLocationInRoom = true, + eventSink = eventsRecorder, + ) + setMessagesView(state = state) + clickOn(CommonStrings.action_stop) + eventsRecorder.assertSingle(MessagesEvent.StopLiveLocationShare) + } + + @Test + fun `clicking live location banner emit expected event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val state = aMessagesState( + isCurrentlySharingLiveLocationInRoom = true, + eventSink = eventsRecorder, + ) + setMessagesView(state = state) + clickOn(CommonStrings.screen_room_live_location_banner) + eventsRecorder.assertSingle(MessagesEvent.ShowLiveLocationShare) } } -private fun AndroidComposeTestRule.setMessagesView( +private fun AndroidComposeUiTest.setMessagesView( state: MessagesState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomDetailsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 8c7f290441..20b636081a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -17,6 +17,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -1169,7 +1170,7 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemRtcNotificationContent(callIntent = CallIntent.VIDEO), + content = TimelineItemRtcNotificationContent(callIntent = CallIntent.VIDEO, state = RtcNotificationState.Started), ) initialState.eventSink.invoke( ActionListEvent.ComputeForMessage( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index 384b78471d..3efdec78ae 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -11,12 +11,19 @@ package io.element.android.features.messages.impl.attachments import android.net.Uri +import androidx.core.net.toUri import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvent import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter import io.element.android.features.messages.impl.attachments.preview.OnDoneListener import io.element.android.features.messages.impl.attachments.preview.SendActionState +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEditor +import io.element.android.features.messages.impl.attachments.preview.imageeditor.AttachmentImageEdits +import io.element.android.features.messages.impl.attachments.preview.imageeditor.EditedLocalMedia +import io.element.android.features.messages.impl.attachments.preview.imageeditor.NormalizedCropRect +import io.element.android.features.messages.impl.attachments.preview.imageeditor.assertIsSimilarTo import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState +import io.element.android.features.messages.impl.attachments.video.VideoCompressionPresetSelector import io.element.android.features.messages.impl.attachments.video.VideoUploadEstimation import io.element.android.features.messages.impl.fixtures.aMediaAttachment import io.element.android.features.messages.test.attachments.video.FakeMediaOptimizationSelectorPresenterFactory @@ -45,6 +52,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfig import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo +import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.preferences.api.store.VideoCompressionPreset @@ -56,6 +64,7 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.every @@ -68,13 +77,11 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import java.io.File +import kotlin.io.path.createTempFile @Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class AttachmentsPreviewPresenterTest { +class AttachmentsPreviewPresenterTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() @@ -548,10 +555,319 @@ class AttachmentsPreviewPresenterTest { } } + @Test + fun `present - applying image edits updates the attachment`() = runTest { + val editedUri = Uri.parse("file:///tmp/edited.jpeg") + val presenter = createAttachmentsPreviewPresenter( + displayMediaQualitySelectorViews = true, + attachmentImageEditor = FakeAttachmentImageEditor { + Result.success( + EditedLocalMedia( + localMedia = aLocalMedia(uri = editedUri), + file = File("/tmp/edited.jpeg"), + ) + ) + } + ) + + presenter.test { + val initialState = awaitItem() + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = awaitItem() + assertThat(editorState.imageEditorState).isNotNull() + + editorState.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) + val rotatedState = awaitItem() + assertThat(rotatedState.imageEditorState?.edits?.rotationQuarterTurns).isEqualTo(3) + + rotatedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits) + assertThat(awaitItem().isApplyingImageEdits).isTrue() + + val appliedState = awaitItem() + assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri) + assertThat(appliedState.imageEditorState).isNull() + assertThat(appliedState.isApplyingImageEdits).isFalse() + } + } + + @Test + fun `present - reopening image editor keeps original media and previous edits`() = runTest { + val editedUri = Uri.parse("file:///tmp/edited.jpeg") + val originalLocalMedia = aLocalMedia(uri = mockMediaUrl) + val cropRect = NormalizedCropRect( + left = 0.2f, + top = 0.15f, + right = 0.85f, + bottom = 0.9f, + ) + val presenter = createAttachmentsPreviewPresenter( + localMedia = originalLocalMedia, + displayMediaQualitySelectorViews = true, + attachmentImageEditor = FakeAttachmentImageEditor { + Result.success( + EditedLocalMedia( + localMedia = aLocalMedia(uri = editedUri), + file = File("/tmp/edited.jpeg"), + ) + ) + } + ) + + presenter.test { + val initialState = awaitItem() + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + + editorState.eventSink(AttachmentsPreviewEvent.UpdateImageCropRect(cropRect)) + val croppedState = awaitItem() + croppedState.eventSink(AttachmentsPreviewEvent.RotateImageToTheLeft) + val rotatedState = awaitItem() + rotatedState.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally) + val flippedHorizontallyState = awaitItem() + flippedHorizontallyState.eventSink(AttachmentsPreviewEvent.FlipImageVertically) + val flippedState = awaitItem() + flippedState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits) + + val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last() + assertThat((appliedState.attachment as Attachment.Media).localMedia.uri).isEqualTo(editedUri) + + appliedState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val reopenedState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + assertThat(reopenedState.imageEditorState!!.localMedia.uri).isEqualTo(originalLocalMedia.uri) + val rotatedCropRect = NormalizedCropRect( + left = cropRect.top, + top = 1f - cropRect.right, + right = cropRect.bottom, + bottom = 1f - cropRect.left, + ) + val flippedCropRect = NormalizedCropRect( + left = 1f - rotatedCropRect.right, + top = 1f - rotatedCropRect.bottom, + right = 1f - rotatedCropRect.left, + bottom = 1f - rotatedCropRect.top, + ) + reopenedState.imageEditorState.edits.cropRect.assertIsSimilarTo(flippedCropRect) + assertThat(reopenedState.imageEditorState.edits.rotationQuarterTurns).isEqualTo(3) + assertThat(reopenedState.imageEditorState.edits.rotationDegrees).isEqualTo(270) + assertThat(reopenedState.imageEditorState.edits.isFlippedHorizontally).isTrue() + assertThat(reopenedState.imageEditorState.edits.isFlippedVertically).isTrue() + } + } + + @Test + fun `present - image editor flip events update edits`() = runTest { + val presenter = createAttachmentsPreviewPresenter(displayMediaQualitySelectorViews = true) + + presenter.test { + val initialState = awaitItem() + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + + editorState.eventSink(AttachmentsPreviewEvent.FlipImageHorizontally) + val flippedHorizontallyState = awaitItem() + assertThat(flippedHorizontallyState.imageEditorState?.edits?.isFlippedHorizontally).isTrue() + + flippedHorizontallyState.eventSink(AttachmentsPreviewEvent.FlipImageVertically) + val flippedState = awaitItem() + assertThat(flippedState.imageEditorState?.edits?.isFlippedVertically).isTrue() + } + } + + fun `present - sendAsFile attachment is pre-processed without image compression`() = runTest { + // Even though the user has enabled "Optimize media quality" globally, picking the file + // through the Files picker (sendAsFile = true) must skip compression. Regression test + // for https://github.com/element-hq/element-x-android/issues/6365 + val mediaPreProcessor = FakeMediaPreProcessor() + val presenter = createAttachmentsPreviewPresenter( + localMedia = aLocalMedia(mockMediaUrl, anImageMediaInfo()), + sendAsFile = true, + mediaPreProcessor = mediaPreProcessor, + // Selector views are hidden in the sendAsFile flow, which triggers the auto pre-process path. + displayMediaQualitySelectorViews = false, + mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider( + config = MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ) + ), + ) + + presenter.test { + consumeItemsUntilPredicate { mediaPreProcessor.processCallCount > 0 } + assertThat(mediaPreProcessor.lastMediaOptimizationConfig).isEqualTo( + MediaOptimizationConfig( + compressImages = false, + videoCompressionPreset = VideoCompressionPreset.HIGH, + ) + ) + } + } + + @Test + fun `present - sending edited media keeps the edited file available until upload starts`() = runTest { + val editedFile = createTempFile(suffix = ".jpeg").toFile().apply { + writeText("edited-media") + } + val sendFileResult = + lambdaRecorder> { file, _, _, _, _ -> + assertThat(file.exists()).isTrue() + Result.success(FakeMediaUploadHandler()) + } + val room = FakeJoinedRoom( + liveTimeline = FakeTimeline().apply { + sendFileLambda = sendFileResult + }, + ) + val presenter = createAttachmentsPreviewPresenter( + room = room, + displayMediaQualitySelectorViews = true, + onDoneListener = OnDoneListener {}, + mediaPreProcessor = FakeMediaPreProcessor().apply { + givenResult( + Result.success( + MediaUploadInfo.AnyFile( + file = editedFile, + fileInfo = FileInfo( + mimetype = MimeTypes.Jpeg, + size = editedFile.length(), + thumbnailInfo = null, + thumbnailSource = null, + ) + ) + ) + ) + }, + attachmentImageEditor = FakeAttachmentImageEditor { + Result.success( + EditedLocalMedia( + localMedia = aLocalMedia(uri = editedFile.toUri()), + file = editedFile, + ) + ) + } + ) + + presenter.test { + val initialState = awaitItem() + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + + editorState.eventSink(AttachmentsPreviewEvent.ApplyImageEdits) + val appliedState = consumeItemsUntilPredicate { !it.isApplyingImageEdits && it.imageEditorState == null }.last() + + appliedState.eventSink(AttachmentsPreviewEvent.SendAttachment) + consumeItemsUntilPredicate { it.sendActionState == SendActionState.Done } + + sendFileResult.assertions().isCalledOnce() + } + } + + @Test + fun `present - image with generic mime type and png extension is still editable`() = runTest { + val localMedia = aLocalMedia( + uri = mockMediaUrl, + mediaInfo = anImageMediaInfo().copy( + mimeType = MimeTypes.OctetStream, + filename = "Screenshot.png", + fileExtension = "png", + ), + ) + val presenter = createAttachmentsPreviewPresenter(localMedia = localMedia) + + presenter.test { + val initialState = awaitItem() + assertThat(initialState.canEditImage).isTrue() + + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + assertThat(editorState.imageEditorState).isNotNull() + } + } + + @Test + fun `present - image can still be edited when editor can decode it despite generic media info`() = runTest { + val localMedia = aLocalMedia( + uri = mockMediaUrl, + mediaInfo = anImageMediaInfo().copy( + mimeType = MimeTypes.OctetStream, + filename = "", + fileExtension = "", + ), + ) + val presenter = createAttachmentsPreviewPresenter( + localMedia = localMedia, + attachmentImageEditor = FakeAttachmentImageEditor( + canEditResult = true, + ) { + Result.success( + EditedLocalMedia( + localMedia = localMedia.copy(uri = Uri.parse("file:///tmp/decoded.jpeg")), + file = File("/tmp/decoded.jpeg"), + ) + ) + } + ) + + presenter.test { + val initialState = consumeItemsUntilPredicate { it.canEditImage }.last() + assertThat(initialState.canEditImage).isTrue() + + initialState.eventSink(AttachmentsPreviewEvent.OpenImageEditor) + val editorState = consumeItemsUntilPredicate { it.imageEditorState != null }.last() + assertThat(editorState.imageEditorState).isNotNull() + } + } + + @Test + fun `present - sendAsFile video is pre-processed with best fitting preset`() = runTest { + val mediaPreProcessor = FakeMediaPreProcessor() + val presenter = createAttachmentsPreviewPresenter( + localMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()), + sendAsFile = true, + mediaPreProcessor = mediaPreProcessor, + // Selector views are hidden in the sendAsFile flow, which triggers the auto pre-process path. + displayMediaQualitySelectorViews = false, + mediaOptimizationSelectorPresenterFactory = FakeMediaOptimizationSelectorPresenterFactory { + MediaOptimizationSelectorState( + maxUploadSize = AsyncData.Success(250_000_000L), + videoSizeEstimations = AsyncData.Success( + persistentListOf( + VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 513_216_000L, canUpload = false), + VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 228_096_000L, canUpload = true), + VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 57_024_000L, canUpload = true), + ) + ), + isImageOptimizationEnabled = false, + selectedVideoPreset = VideoCompressionPreset.STANDARD, + displayMediaSelectorViews = false, + displayVideoPresetSelectorDialog = false, + eventSink = {}, + ) + }, + mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider( + config = MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.LOW, + ) + ), + ) + + presenter.test { + consumeItemsUntilPredicate { mediaPreProcessor.processCallCount > 0 } + assertThat(mediaPreProcessor.lastMediaOptimizationConfig).isEqualTo( + MediaOptimizationConfig( + compressImages = false, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ) + ) + } + } + private fun TestScope.createAttachmentsPreviewPresenter( localMedia: LocalMedia = aLocalMedia( uri = mockMediaUrl, ), + sendAsFile: Boolean = false, room: JoinedRoom = FakeJoinedRoom(), timelineMode: Timeline.Mode = Timeline.Mode.Live, permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(), @@ -573,9 +889,18 @@ class AttachmentsPreviewPresenterTest { } ), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), + attachmentImageEditor: AttachmentImageEditor = FakeAttachmentImageEditor { + Result.success( + EditedLocalMedia( + localMedia = localMedia.copy(uri = Uri.parse("file:///tmp/default-edited.jpeg")), + file = File("/tmp/default-edited.jpeg"), + ) + ) + }, + videoCompressionPresetSelector: VideoCompressionPresetSelector = VideoCompressionPresetSelector(), ): AttachmentsPreviewPresenter { return AttachmentsPreviewPresenter( - attachment = aMediaAttachment(localMedia), + attachment = aMediaAttachment(localMedia, sendAsFile = sendAsFile), onDoneListener = onDoneListener, mediaSenderFactory = MediaSenderFactory { timelineMode -> DefaultMediaSender( @@ -589,15 +914,33 @@ class AttachmentsPreviewPresenterTest { }, permalinkBuilder = permalinkBuilder, temporaryUriDeleter = temporaryUriDeleter, + attachmentImageEditor = attachmentImageEditor, sessionCoroutineScope = this, dispatchers = testCoroutineDispatchers(), mediaOptimizationSelectorPresenterFactory = mediaOptimizationSelectorPresenterFactory, + videoCompressionPresetSelector = videoCompressionPresetSelector, timelineMode = timelineMode, inReplyToEventId = null, mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, ) } + private class FakeAttachmentImageEditor( + private val canEditResult: Boolean = true, + private val result: () -> Result, + ) : AttachmentImageEditor { + override suspend fun canEdit(localMedia: LocalMedia): Boolean { + return canEditResult + } + + override suspend fun exportEdits( + localMedia: LocalMedia, + edits: AttachmentImageEdits, + ): Result { + return result() + } + } + private val mediaUploadInfo = MediaUploadInfo.AnyFile( File("test"), FileInfo( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditsTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditsTest.kt new file mode 100644 index 0000000000..eb989967ef --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditsTest.kt @@ -0,0 +1,78 @@ +/* + * 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.attachments.preview.imageeditor + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class AttachmentImageEditsTest { + @Test + fun `rotate normalizes after a full turn`() { + var edits = AttachmentImageEdits() + repeat(4) { + edits = edits.rotateAntiClockwise() + } + assertThat(edits.normalizedRotationQuarterTurns).isEqualTo(0) + assertThat(edits.rotationDegrees).isEqualTo(0) + assertThat(edits.hasChanges).isFalse() + } + + @Test + fun `rotate updates rotation and crop`() { + val sut = AttachmentImageEdits( + cropRect = NormalizedCropRect( + left = 0.2f, + top = 0.3f, + right = 0.8f, + bottom = 0.9f, + ), + rotationQuarterTurns = 0, + ) + val result = sut.rotateAntiClockwise() + assertThat(result.normalizedRotationQuarterTurns).isEqualTo(3) + assertThat(result.rotationDegrees).isEqualTo(270) + assertThat(result.cropRect.left).isWithin(0.0001f).of(0.3f) + assertThat(result.cropRect.top).isWithin(0.0001f).of(0.2f) + assertThat(result.cropRect.right).isWithin(0.0001f).of(0.9f) + assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.8f) + assertThat(result.hasChanges).isTrue() + } + + @Test + fun `flip horizontally updates crop and change tracking`() { + val sut = AttachmentImageEdits( + cropRect = NormalizedCropRect( + left = 0.1f, + top = 0.3f, + right = 0.6f, + bottom = 0.9f, + ) + ) + val result = sut.flipHorizontally() + assertThat(result.isFlippedHorizontally).isTrue() + assertThat(result.cropRect.left).isWithin(0.0001f).of(0.4f) + assertThat(result.cropRect.right).isWithin(0.0001f).of(0.9f) + assertThat(result.cropRect.top).isWithin(0.0001f).of(0.3f) + assertThat(result.cropRect.bottom).isWithin(0.0001f).of(0.9f) + assertThat(result.hasChanges).isTrue() + } + + @Test + fun `flip vertical twice resets to default state`() { + val edits = AttachmentImageEdits().flipVertically().flipVertically() + assertThat(edits.isFlippedVertically).isFalse() + assertThat(edits.hasChanges).isFalse() + } + + @Test + fun `flip horizontally twice resets to default state`() { + val edits = AttachmentImageEdits().flipHorizontally().flipHorizontally() + assertThat(edits.isFlippedVertically).isFalse() + assertThat(edits.hasChanges).isFalse() + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/DefaultAttachmentImageEditorTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/DefaultAttachmentImageEditorTest.kt new file mode 100644 index 0000000000..f9db4f35f9 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/DefaultAttachmentImageEditorTest.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.messages.impl.attachments.preview.imageeditor + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes +import org.junit.Test + +class DefaultAttachmentImageEditorTest { + @Test + fun `exported mime type preserves png`() { + assertThat(exportedMimeTypeFor(MimeTypes.Png)).isEqualTo(MimeTypes.Png) + } + + @Test + fun `exported mime type normalizes non-png images to jpeg`() { + assertThat(exportedMimeTypeFor("image/heic")).isEqualTo(MimeTypes.Jpeg) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/NormalizedCropRectTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/NormalizedCropRectTest.kt new file mode 100644 index 0000000000..c70c6169e1 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/NormalizedCropRectTest.kt @@ -0,0 +1,180 @@ +/* + * 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.attachments.preview.imageeditor + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class NormalizedCropRectTest { + private val rect = NormalizedCropRect( + left = 0.1f, + top = 0.2f, + right = 0.7f, + bottom = 0.8f, + ) + + @Test + fun `applyChange with top handle only updates the top edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Edge.Top, + deltaX = 0.3f, + deltaY = 0.1f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = rect.left, + top = 0.3f, + right = rect.right, + bottom = rect.bottom, + ) + ) + } + + @Test + fun `applyChange with left handle only updates the left edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Edge.Left, + deltaX = 0.1f, + deltaY = 0.3f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = 0.2f, + top = rect.top, + right = rect.right, + bottom = rect.bottom, + ) + ) + } + + @Test + fun `applyChange with right handle only updates the right edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Edge.Right, + deltaX = -0.1f, + deltaY = 0.3f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = rect.left, + top = rect.top, + right = 0.6f, + bottom = rect.bottom, + ) + ) + } + + @Test + fun `applyChange with bottom handle target only updates the bottem edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Edge.Bottom, + deltaX = -0.1f, + deltaY = -0.3f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = rect.left, + top = rect.top, + right = rect.right, + bottom = 0.5f, + ) + ) + } + + @Test + fun `applyChange with top left handle updates the top and left bottem edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Corner.TopLeft, + deltaX = 0.1f, + deltaY = 0.1f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = 0.2f, + top = 0.3f, + right = rect.right, + bottom = rect.bottom, + ) + ) + } + + @Test + fun `applyChange with top right handle updates the top and right bottem edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Corner.TopRight, + deltaX = -0.1f, + deltaY = 0.1f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = rect.left, + top = 0.3f, + right = 0.6f, + bottom = rect.bottom, + ) + ) + } + + @Test + fun `applyChange with bottom left handle updates the bottom and left bottem edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Corner.BottomLeft, + deltaX = 0.1f, + deltaY = -0.1f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = 0.2f, + top = rect.top, + right = rect.right, + bottom = 0.7f, + ) + ) + } + + @Test + fun `applyChange with bottom right handle updates the bottom and right bottem edge`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Corner.BottomRight, + deltaX = -0.1f, + deltaY = -0.1f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = rect.left, + top = rect.top, + right = 0.6f, + bottom = 0.7f, + ) + ) + } + + @Test + fun `translate keeps the crop rect inside bounds`() { + val result = rect.applyChange( + dragTarget = CropDragTarget.Move, + deltaX = 0.6f, + deltaY = 0.6f, + ) + result.assertIsSimilarTo( + NormalizedCropRect( + left = 0.4f, + top = 0.4f, + right = 1.0f, + bottom = 1.0f, + ) + ) + } +} + +internal fun NormalizedCropRect.assertIsSimilarTo(expectedResult: NormalizedCropRect) { + assertThat(left).isWithin(0.0001f).of(expectedResult.left) + assertThat(top).isWithin(0.0001f).of(expectedResult.top) + assertThat(right).isWithin(0.0001f).of(expectedResult.right) + assertThat(bottom).isWithin(0.0001f).of(expectedResult.bottom) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt index 106fff7375..99483bdac4 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenterTest.kt @@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.attachments.video import android.net.Uri import android.util.Size -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.test.attachments.video.FakeVideoMetadataExtractor import io.element.android.features.messages.test.attachments.video.FakeVideoMetadataExtractorFactory @@ -26,16 +25,15 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.test import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith import kotlin.time.Duration.Companion.minutes -@RunWith(AndroidJUnit4::class) -class DefaultMediaOptimizationSelectorPresenterTest { +class DefaultMediaOptimizationSelectorPresenterTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() @@ -210,19 +208,88 @@ class DefaultMediaOptimizationSelectorPresenterTest { } } + @Test + fun `present - sendAsFile hides selector views and disables image compression for images`() = runTest { + val presenter = createDefaultMediaOptimizationSelectorPresenter( + localMedia = aLocalMedia(mockMediaUrl, anImageMediaInfo()), + // Even with the feature flag on, sendAsFile must hide the selector. + featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)), + // And it must override the user's "optimize images" preference. + mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), + sendAsFile = true, + ) + presenter.test { + // Initial loading state + skipItems(1) + awaitItem().run { + assertThat(displayMediaSelectorViews).isFalse() + assertThat(isImageOptimizationEnabled).isFalse() + } + } + } + + @Test + fun `present - sendAsFile picks HIGH video preset when the video fits the upload limit`() = runTest { + val presenter = createDefaultMediaOptimizationSelectorPresenter( + // Plenty of room: even HIGH preset will fit. + maxUploadSizeProvider = MaxUploadSizeProvider { Result.success(Long.MAX_VALUE) }, + mediaExtractorFactory = FakeVideoMetadataExtractorFactory( + FakeVideoMetadataExtractor( + sizeResult = Result.success(Size(1920, 1080)), + duration = Result.success(10.minutes) + ) + ), + sendAsFile = true, + ) + presenter.test { + // Initial loading state, then the one with size estimations loaded. + skipItems(1) + awaitItem().run { + assertThat(displayMediaSelectorViews).isFalse() + assertThat(selectedVideoPreset).isEqualTo(VideoCompressionPreset.HIGH) + } + } + } + + @Test + fun `present - sendAsFile picks lower video preset when HIGH exceeds the upload limit`() = runTest { + val presenter = createDefaultMediaOptimizationSelectorPresenter( + maxUploadSizeProvider = MaxUploadSizeProvider { Result.success(250_000_000L) }, + mediaExtractorFactory = FakeVideoMetadataExtractorFactory( + FakeVideoMetadataExtractor( + sizeResult = Result.success(Size(1920, 1080)), + duration = Result.success(10.minutes) + ) + ), + sendAsFile = true, + ) + presenter.test { + // Initial loading state, then the one with size estimations loaded. + skipItems(1) + awaitItem().run { + assertThat(displayMediaSelectorViews).isFalse() + assertThat(selectedVideoPreset).isEqualTo(VideoCompressionPreset.STANDARD) + } + } + } + private fun createDefaultMediaOptimizationSelectorPresenter( localMedia: LocalMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()), maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider { Result.success(1_000L) }, featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)), mediaExtractorFactory: FakeVideoMetadataExtractorFactory = FakeVideoMetadataExtractorFactory(), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), + videoCompressionPresetSelector: VideoCompressionPresetSelector = VideoCompressionPresetSelector(), + sendAsFile: Boolean = false, ): DefaultMediaOptimizationSelectorPresenter { return DefaultMediaOptimizationSelectorPresenter( localMedia = localMedia, + sendAsFile = sendAsFile, maxUploadSizeProvider = maxUploadSizeProvider, featureFlagService = featureFlagService, mediaExtractorFactory = mediaExtractorFactory, mediaOptimizationConfigProvider = mediaOptimizationConfigProvider, + videoCompressionPresetSelector = videoCompressionPresetSelector, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/VideoCompressionPresetSelectorTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/VideoCompressionPresetSelectorTest.kt new file mode 100644 index 0000000000..d3864794c2 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/video/VideoCompressionPresetSelectorTest.kt @@ -0,0 +1,92 @@ +/* + * 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.attachments.video + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.preferences.api.store.VideoCompressionPreset +import kotlinx.collections.immutable.persistentListOf +import org.junit.Test + +class VideoCompressionPresetSelectorTest { + private val selector = VideoCompressionPresetSelector() + + @Test + fun `selectBestVideoPreset - returns expected preset when it can upload`() { + val result = selector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.HIGH, + videoSizeEstimations = AsyncData.Success( + persistentListOf( + VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = true), + VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = true), + VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = true), + ) + ) + ) + + assertThat(result.dataOrNull()).isEqualTo(VideoCompressionPreset.HIGH) + } + + @Test + fun `selectBestVideoPreset - falls back to the highest fitting preset`() { + val result = selector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.HIGH, + videoSizeEstimations = AsyncData.Success( + persistentListOf( + VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = false), + VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = true), + VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = true), + ) + ) + ) + + assertThat(result.dataOrNull()).isEqualTo(VideoCompressionPreset.STANDARD) + } + + @Test + fun `selectBestVideoPreset - starts from the expected preset`() { + val result = selector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.STANDARD, + videoSizeEstimations = AsyncData.Success( + persistentListOf( + VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = true), + VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = true), + VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = true), + ) + ) + ) + + assertThat(result.dataOrNull()).isEqualTo(VideoCompressionPreset.STANDARD) + } + + @Test + fun `selectBestVideoPreset - returns failure when no preset can upload`() { + val result = selector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.HIGH, + videoSizeEstimations = AsyncData.Success( + persistentListOf( + VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = false), + VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = false), + VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = false), + ) + ) + ) + + assertThat(result).isInstanceOf(AsyncData.Failure::class.java) + } + + @Test + fun `selectBestVideoPreset - returns loading while estimations are missing`() { + val result = selector.selectBestVideoPreset( + expectedVideoPreset = VideoCompressionPreset.HIGH, + videoSizeEstimations = AsyncData.Loading(), + ) + + assertThat(result).isInstanceOf(AsyncData.Loading::class.java) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt index 24779ba78a..4d29f10a57 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateViewTest.kt @@ -6,13 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.crypto.identity import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -21,19 +23,14 @@ import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class IdentityChangeStateViewTest { - @get:Rule val rule = createAndroidComposeRule() +class IdentityChangeStateViewTest : RobolectricTest() { @Test - fun `show and resolve pin violation`() { + fun `show and resolve pin violation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIdentityChangeStateView( + setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -45,18 +42,18 @@ class IdentityChangeStateViewTest { ), ) - rule.onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning") - rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") - rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname") + onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning") + onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") + onNodeWithText("Alice", substring = true).assertExists("should display user displayname") - rule.clickOn(res = CommonStrings.action_dismiss) + clickOn(res = CommonStrings.action_dismiss) eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost"))) } @Test - fun `show and resolve verification violation`() { + fun `show and resolve verification violation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIdentityChangeStateView( + setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -68,17 +65,17 @@ class IdentityChangeStateViewTest { ), ) - rule.onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning") - rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") - rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname") + onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning") + onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid") + onNodeWithText("Alice", substring = true).assertExists("should display user displayname") - rule.clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action) + clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action) eventsRecorder.assertSingle(IdentityChangeEvent.WithdrawVerification(UserId("@alice:localhost"))) } @Test - fun `Should not show any banner if no violations`() { - rule.setIdentityChangeStateView( + fun `Should not show any banner if no violations`() = runAndroidComposeUiTest { + setIdentityChangeStateView( state = anIdentityChangeState( listOf( RoomMemberIdentityStateChange( @@ -93,10 +90,10 @@ class IdentityChangeStateViewTest { ), ) - rule.onNodeWithText("identity was reset", substring = true).assertDoesNotExist() + onNodeWithText("identity was reset", substring = true).assertDoesNotExist() } - private fun AndroidComposeTestRule.setIdentityChangeStateView( + private fun AndroidComposeUiTest.setIdentityChangeStateView( state: IdentityChangeState, ) { setContent { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt index 02767fbeb9..6fe01bb281 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureViewTest.kt @@ -6,54 +6,51 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.crypto.sendfailure.resolve 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ResolveVerifiedUserSendFailureViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ResolveVerifiedUserSendFailureViewTest : RobolectricTest() { @Test - fun `clicking on resolve and resend emit the expected event`() { + fun `clicking on resolve and resend emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResolveVerifiedUserSendFailureView( + setResolveVerifiedUserSendFailureView( state = aResolveVerifiedUserSendFailureState( verifiedUserSendFailure = aChangedIdentitySendFailure(), eventSink = eventsRecorder, ), ) - rule.clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) + clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.ResolveAndResend) } @Test - fun `clicking on retry emit the expected event`() { + fun `clicking on retry emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResolveVerifiedUserSendFailureView( + setResolveVerifiedUserSendFailureView( state = aResolveVerifiedUserSendFailureState( verifiedUserSendFailure = aChangedIdentitySendFailure(), eventSink = eventsRecorder, ), ) - rule.clickOn(res = CommonStrings.action_retry) + clickOn(res = CommonStrings.action_retry) eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.Retry) } - private fun AndroidComposeTestRule.setResolveVerifiedUserSendFailureView( + private fun AndroidComposeUiTest.setResolveVerifiedUserSendFailureView( state: ResolveVerifiedUserSendFailureState, ) { setSafeContent { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt index 77207d6b52..1dde33714f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt @@ -11,6 +11,7 @@ package io.element.android.features.messages.impl.fixtures import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.mediaviewer.api.local.LocalMedia -fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media( +fun aMediaAttachment(localMedia: LocalMedia, sendAsFile: Boolean = false) = Attachment.Media( localMedia = localMedia, + sendAsFile = sendAsFile, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt index e198ea9043..69093e948c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/link/LinkViewTest.kt @@ -6,64 +6,60 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.link 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.wysiwyg.link.Link -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class LinkViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class LinkViewTest : RobolectricTest() { @Test - fun `clicking on cancel emits the expected event`() { + fun `clicking on cancel emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLinkView( + setLinkView( aLinkState( linkClick = ConfirmingLinkClick(aLink), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle( LinkEvent.Cancel ) } @Test - fun `clicking on continue emits the expected event`() { + fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setLinkView( + setLinkView( aLinkState( linkClick = ConfirmingLinkClick(aLink), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( LinkEvent.Confirm ) } @Test - fun `success state invokes the callback and emits the expected event`() { + fun `success state invokes the callback and emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(aLink) { callback -> - rule.setLinkView( + setLinkView( aLinkState( linkClick = AsyncAction.Success(aLink), eventSink = eventsRecorder, @@ -77,7 +73,7 @@ class LinkViewTest { } } -private fun AndroidComposeTestRule.setLinkView( +private fun AndroidComposeUiTest.setLinkView( state: LinkState, onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(), ) { 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 index 116a1cfb5d..0b3da9647e 100644 --- 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 @@ -24,6 +24,7 @@ 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.ThreadId 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 @@ -271,12 +272,12 @@ class MessageComposerPresenterSlashCommandTest { isRichTextEditorEnabled: Boolean = true, draftService: ComposerDraftService = FakeComposerDraftService(), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), - isInThread: Boolean = false, + threadRoot: ThreadId? = null, slashCommandService: SlashCommandService = FakeSlashCommandService(), ) = MessageComposerPresenter( navigator = navigator, sessionCoroutineScope = this, - isInThread = isInThread, + threadRoot = threadRoot, room = room, mediaPickerProvider = pickerProvider, sessionPreferencesStore = sessionPreferencesStore, 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 7a2cc1110a..0f2a2ce32b 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 @@ -107,6 +107,7 @@ import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.test import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk @@ -118,14 +119,11 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import uniffi.wysiwyg_composer.MentionsState import java.io.File @Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class MessageComposerPresenterTest { +class MessageComposerPresenterTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() @@ -1066,7 +1064,7 @@ class MessageComposerPresenterTest { ) givenRoomInfo( aRoomInfo( - isDirect = true, + isDm = true, activeMembersCount = 2, ) ) @@ -1534,12 +1532,12 @@ class MessageComposerPresenterTest { isRichTextEditorEnabled: Boolean = true, draftService: ComposerDraftService = FakeComposerDraftService(), mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(), - isInThread: Boolean = false, + threadRoot: ThreadId? = null, slashCommandService: SlashCommandService = FakeSlashCommandService(), ) = MessageComposerPresenter( navigator = navigator, sessionCoroutineScope = this, - isInThread = isInThread, + threadRoot = threadRoot, room = room, mediaPickerProvider = pickerProvider, sessionPreferencesStore = sessionPreferencesStore, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt index 2c33e348c0..cbe76d73f7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.pinned.banner import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -22,49 +24,44 @@ 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.ensureCalledOnceWithParam -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PinnedMessagesBannerViewTest { - @get:Rule val rule = createAndroidComposeRule() +class PinnedMessagesBannerViewTest : RobolectricTest() { @Test - fun `clicking on the banner invoke expected callback`() { + fun `clicking on the banner invoke expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aLoadedPinnedMessagesBannerState( eventSink = eventsRecorder ) val pinnedEventId = state.currentPinnedMessage.eventId ensureCalledOnceWithParam(pinnedEventId) { callback -> - rule.setPinnedMessagesBannerView( + setPinnedMessagesBannerView( state = state, onClick = callback ) - rule.onRoot().performClick() + onRoot().performClick() eventsRecorder.assertSingle(PinnedMessagesBannerEvent.MoveToNextPinned) } } @Test - fun `clicking on view all emit the expected event`() { + fun `clicking on view all emit the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = true) val state = aLoadedPinnedMessagesBannerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setPinnedMessagesBannerView( + setPinnedMessagesBannerView( state = state, onViewAllClick = callback ) - rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) + clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) } } } -private fun AndroidComposeTestRule.setPinnedMessagesBannerView( +private fun AndroidComposeUiTest.setPinnedMessagesBannerView( state: PinnedMessagesBannerState, onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(), onViewAllClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 41671b71c1..393dcada18 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -6,17 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.pinned.list import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.messages.impl.actionlist.ActionListEvent import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.timeline.aTimelineItemList @@ -29,35 +31,29 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent import io.element.android.wysiwyg.link.Link -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PinnedMessagesListViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class PinnedMessagesListViewTest : RobolectricTest() { @Test - fun `clicking on back calls the expected callback`() { + fun `clicking on back calls the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aLoadedPinnedMessagesListState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setPinnedMessagesListView( + setPinnedMessagesListView( state = state, onBackClick = callback ) - rule.pressBack() + pressBack() } } @Test - fun `click on an event calls the expected callback`() { + fun `click on an event calls the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val content = aTimelineItemFileContent() val state = aLoadedPinnedMessagesListState( @@ -67,16 +63,16 @@ class PinnedMessagesListViewTest { val event = state.timelineItems.first() as TimelineItem.Event ensureCalledOnceWithParam(event) { callback -> - rule.setPinnedMessagesListView( + setPinnedMessagesListView( state = state, onEventClick = callback ) - rule.onAllNodesWithText(content.filename).onFirst().performClick() + onAllNodesWithText(content.filename).onFirst().performClick() } } @Test - fun `long click on an event emits the expected event`() { + fun `long click on an event emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = true) val content = aTimelineItemFileContent() val state = aLoadedPinnedMessagesListState( @@ -84,10 +80,10 @@ class PinnedMessagesListViewTest { actionListState = anActionListState(eventSink = eventsRecorder) ) - rule.setPinnedMessagesListView( + setPinnedMessagesListView( state = state, ) - rule.onAllNodesWithText(content.filename).onFirst() + onAllNodesWithText(content.filename).onFirst() .performTouchInput { longClick() } @@ -96,7 +92,7 @@ class PinnedMessagesListViewTest { } } -private fun AndroidComposeTestRule.setPinnedMessagesListView( +private fun AndroidComposeUiTest.setPinnedMessagesListView( state: PinnedMessagesListState, onBackClick: () -> Unit = EnsureNeverCalled(), onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt index 315d9c459c..3c339aa04e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt @@ -6,11 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runComposeUiTest import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -18,15 +21,10 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class DefaultHtmlConverterProviderTest { - @get:Rule val composeTestRule = createComposeRule() +class DefaultHtmlConverterProviderTest : RobolectricTest() { private val provider = DefaultHtmlConverterProvider( mentionSpanProvider = MentionSpanProvider( permalinkParser = FakePermalinkParser(), @@ -43,8 +41,8 @@ class DefaultHtmlConverterProviderTest { } @Test - fun `calling provide after calling Update first should return an HtmlConverter`() { - composeTestRule.setContent { + fun `calling provide after calling Update first should return an HtmlConverter`() = runComposeUiTest { + setContent { CompositionLocalProvider(LocalInspectionMode provides true) { provider.Update() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 194694714b..af37fb61ef 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.messages.impl.timeline import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.messages.impl.FakeMessagesNavigator import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.fixtures.aMessageEvent @@ -1012,6 +1013,7 @@ class TimelinePresenterTest { sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), + liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -1030,6 +1032,7 @@ class TimelinePresenterTest { roomCallStatePresenter = { aStandByCallState() }, featureFlagService = featureFlagService, analyticsService = FakeAnalyticsService(), + liveLocationShareManager = liveLocationShareManager, ) } } 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 3a0b0e1224..1637dd7a85 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 @@ -6,16 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToIndex -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.messages.impl.timeline.components.MessageShieldData import io.element.android.features.messages.impl.timeline.components.aCriticalShield import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -34,24 +36,19 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent import io.element.android.wysiwyg.link.Link import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import org.junit.Ignore -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class TimelineViewTest { - @get:Rule val rule = createAndroidComposeRule() +class TimelineViewTest : RobolectricTest() { @Test - fun `reaching the end of the timeline with more events to load emits a LoadMore event`() { + fun `reaching the end of the timeline with more events to load emits a LoadMore event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf( TimelineItem.Virtual( @@ -66,9 +63,9 @@ class TimelineViewTest { } @Test - fun `reaching the end of the timeline does not send a LoadMore event`() { + fun `reaching the end of the timeline does not send a LoadMore event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), eventSink = eventsRecorder, @@ -78,9 +75,9 @@ class TimelineViewTest { } @Test - fun `scroll to bottom on live timeline does not emit the Event`() { + fun `scroll to bottom on live timeline does not emit the Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = true, @@ -92,14 +89,14 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom) + onNodeWithContentDescription(contentDescription).performClick() } @Test - fun `scroll to bottom on detached timeline emits the expected Event`() { + fun `scroll to bottom on detached timeline emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, @@ -110,15 +107,15 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom) + onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertSingle(TimelineEvent.JumpToLive) } @Test - fun `an empty timeline triggers a prefetch`() { + fun `an empty timeline triggers a prefetch`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(), eventSink = eventsRecorder, @@ -129,9 +126,9 @@ class TimelineViewTest { } @Test - fun `show shield dialog`() { + fun `show shield dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf( aTimelineItemEvent( @@ -143,8 +140,8 @@ class TimelineViewTest { eventSink = eventsRecorder, ), ) - val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details) - rule.onNodeWithContentDescription(contentDescription).performClick() + val contentDescription = activity!!.getString(CommonStrings.a11y_encryption_details) + onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( TimelineEvent.OnScrollFinished(0), @@ -154,9 +151,9 @@ class TimelineViewTest { } @Test - fun `hide shield dialog`() { + fun `hide shield dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())), isLive = false, @@ -167,16 +164,16 @@ class TimelineViewTest { eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0)) eventsRecorder.clear() - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) } @Ignore( "performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." + - "This needs to be reworked to use a different approach to check the LoadMore event was emitted." + "This needs to be reworked to use a different approach to check the LoadMore event was emitted." ) @Test - fun `scrolling near to the start of the loaded items triggers a pre-fetch`() { + fun `scrolling near to the start of the loaded items triggers a pre-fetch`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val items = List(200) { aTimelineItemEvent( @@ -185,7 +182,7 @@ class TimelineViewTest { ) }.toImmutableList() - rule.setTimelineView( + setTimelineView( state = aTimelineState( timelineItems = items, eventSink = eventsRecorder, @@ -194,9 +191,9 @@ class TimelineViewTest { ), ) - rule.onNodeWithTag("timeline").performScrollToIndex(180) + onNodeWithTag("timeline").performScrollToIndex(180) - rule.mainClock.advanceTimeBy(1000) + mainClock.advanceTimeBy(1000) eventsRecorder.assertList( listOf( @@ -207,7 +204,7 @@ class TimelineViewTest { } } -private fun AndroidComposeTestRule.setTimelineView( +private fun AndroidComposeUiTest.setTimelineView( state: TimelineState, timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(), onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt index 541b16b546..9ebbfd5a3a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/picker/EmojiPickerPresenterTest.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.currentComposer import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow @@ -25,6 +24,7 @@ import io.element.android.emojibasebindings.EmojibaseCategory import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -33,10 +33,8 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class EmojiPickerPresenterTest { +class EmojiPickerPresenterTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt index 64b5216d2e..1d3bb26a93 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt @@ -6,13 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline.components.event import androidx.activity.ComponentActivity +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.libraries.testtags.TestTags @@ -20,14 +22,10 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressTag -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class TimelineItemPollViewTest { - @get:Rule val rule = createAndroidComposeRule() +class TimelineItemPollViewTest : RobolectricTest() { @Test fun `answering a poll with first answer should emit a PollAnswerSelected event`() { testAnswer(answerIndex = 0) @@ -38,17 +36,17 @@ class TimelineItemPollViewTest { testAnswer(answerIndex = 1) } - private fun testAnswer(answerIndex: Int) { + private fun testAnswer(answerIndex: Int) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent() - rule.setContent { + setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } val answer = content.answerItems[answerIndex].answer - rule.onNode( + onNode( matcher = hasText(answer.text), useUnmergedTree = true, ).performClick() @@ -56,38 +54,38 @@ class TimelineItemPollViewTest { } @Test - fun `editing a poll should emit a PollEditClicked event`() { + fun `editing a poll should emit a PollEditClicked event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent( isMine = true, isEditable = true, ) - rule.setContent { + setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } - rule.clickOn(CommonStrings.action_edit_poll) + clickOn(CommonStrings.action_edit_poll) eventsRecorder.assertSingle(TimelineEvent.EditPoll(content.eventId!!)) } @Test - fun `closing a poll should emit a PollEndClicked event`() { + fun `closing a poll should emit a PollEndClicked event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val content = aTimelineItemPollContent( isMine = true, ) - rule.setContent { + setContent { TimelineItemPollView( content = content, eventSink = eventsRecorder ) } - rule.clickOn(CommonStrings.action_end_poll) + clickOn(CommonStrings.action_end_poll) // A confirmation dialog should be shown eventsRecorder.assertEmpty() - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(TimelineEvent.EndPoll(content.eventId!!)) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt index 154225aa7a..d0203d0329 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineTextViewTest.kt @@ -6,17 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline.components.event import android.text.SpannableString import android.text.SpannedString import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.core.text.buildSpannedString import androidx.core.text.inSpans -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -36,47 +38,41 @@ import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.libraries.textcomposer.mentions.getMentionSpans import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.wysiwyg.view.spans.CustomMentionSpan import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.test.runTest -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class TimelineTextViewTest { - @get:Rule val rule = createAndroidComposeRule() +class TimelineTextViewTest : RobolectricTest() { private val mentionSpanTheme = MentionSpanTheme(currentUserId = A_USER_ID) private val formatLambda = lambdaRecorder { mentionType -> mentionType.toString() } private val mentionSpanFormatter = FakeMentionSpanFormatter(formatLambda) @Test - fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runTest { + fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runAndroidComposeUiTest { val charSequence = "Hello @alice:example.com" val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest { + fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runAndroidComposeUiTest { val charSequence = SpannableString("Hello @alice:example.com") val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans()).isEmpty() assert(formatLambda).isNeverCalled() } @Test - fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runTest { + fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runAndroidComposeUiTest { val charSequence = "Hello @alice:example.com" val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null)) assertThat(result.getMentionSpans()).isEmpty() assertThat(result.toString()).isEqualTo(charSequence) @@ -84,7 +80,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - with Room mention format correctly`() = runTest { + fun `getTextWithResolvedMentions - with Room mention format correctly`() = runAndroidComposeUiTest { val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias()) val charSequence = buildSpannedString { append("Hello ") @@ -93,7 +89,7 @@ class TimelineTextViewTest { } } val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val expectedDisplayText = mentionType.toString() assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) @@ -102,7 +98,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest { + fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runAndroidComposeUiTest { val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") @@ -111,7 +107,7 @@ class TimelineTextViewTest { } } val mentionSpanUpdater = aMentionSpanUpdater() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) val expectedDisplayText = mentionType.toString() assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) @@ -119,7 +115,7 @@ class TimelineTextViewTest { } @Test - fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest { + fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runAndroidComposeUiTest { val mentionType = MentionType.User(userId = A_USER_ID) val charSequence = buildSpannedString { append("Hello ") @@ -129,12 +125,12 @@ class TimelineTextViewTest { } val mentionSpanUpdater = aMentionSpanUpdater() val expectedDisplayText = mentionType.toString() - val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) + val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence)) assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText) assert(formatLambda).isCalledOnce() } - private suspend fun AndroidComposeTestRule.getText( + private suspend fun AndroidComposeUiTest.getText( mentionSpanUpdater: MentionSpanUpdater, content: TimelineItemTextBasedContent, ): CharSequence { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index fa837f1492..5453aac205 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -67,18 +67,18 @@ import io.element.android.libraries.matrix.test.timeline.aProfileDetails import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.test.runTest import org.jsoup.nodes.Document import org.junit.Assert.fail import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes -@Suppress("LargeClass") @RunWith(RobolectricTestRunner::class) class TimelineItemContentMessageFactoryTest { +@Suppress("LargeClass") +class TimelineItemContentMessageFactoryTest : RobolectricTest() { @Test fun `test create OtherMessageType`() = runTest { val sut = createTimelineItemContentMessageFactory() @@ -343,10 +343,10 @@ import kotlin.time.Duration.Companion.minutes formattedCaption = null, source = MediaSource("url"), info = AudioInfo( - duration = 1.minutes, - size = 123L, - mimetype = MimeTypes.Mp3, - ) + duration = 1.minutes, + size = 123L, + mimetype = MimeTypes.Mp3, + ) ), isEdited = true, ), @@ -584,16 +584,16 @@ import kotlin.time.Duration.Companion.minutes formattedCaption = null, source = MediaSource("url"), info = FileInfo( - mimetype = MimeTypes.Pdf, - size = 123L, - thumbnailInfo = ThumbnailInfo( - height = 10L, - width = 5L, - mimetype = MimeTypes.Jpeg, - size = 111L, - ), - thumbnailSource = MediaSource("url_thumbnail"), - ) + mimetype = MimeTypes.Pdf, + size = 123L, + thumbnailInfo = ThumbnailInfo( + height = 10L, + width = 5L, + mimetype = MimeTypes.Jpeg, + size = 111L, + ), + thumbnailSource = MediaSource("url_thumbnail"), + ) ), isEdited = true, ), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt index af3acee6a2..aef12deeca 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedViewTest.kt @@ -6,56 +6,53 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.messages.impl.timeline.protection import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.lambda.lambdaError -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ProtectedViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ProtectedViewTest : RobolectricTest() { @Test - fun `when hideContent is false, the content is rendered`() { - rule.setProtectedView( + fun `when hideContent is false, the content is rendered`() = runAndroidComposeUiTest { + setProtectedView( hideContent = false, content = { Text("Hello") } ) - rule.onNodeWithText("Hello").assertExists() + onNodeWithText("Hello").assertExists() } @Test - fun `when hideContent is true, the content is not rendered, and user can reveal it`() { + fun `when hideContent is true, the content is not rendered, and user can reveal it`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setProtectedView( + setProtectedView( hideContent = true, onShowClick = it, content = { Text("Hello") } ) - rule.onNodeWithText("Hello").assertDoesNotExist() - rule.clickOn(CommonStrings.action_show) + onNodeWithText("Hello").assertDoesNotExist() + clickOn(CommonStrings.action_show) } } } -private fun AndroidComposeTestRule.setProtectedView( +private fun AndroidComposeUiTest.setProtectedView( hideContent: Boolean = false, onShowClick: () -> Unit = { lambdaError() }, content: @Composable () -> Unit = {}, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt index 645039b30c..c5709588f0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionStateTest.kt @@ -42,4 +42,19 @@ class TimelineProtectionStateTest { assertThat(sut.hideMediaContent(AN_EVENT_ID)).isFalse() assertThat(sut.hideMediaContent(AN_EVENT_ID_2)).isTrue() } + + @Test + fun `when isMine is true, hideMediaContent always returns false regardless of state`() { + val sutRenderAll = aTimelineProtectionState( + protectionState = ProtectionState.RenderAll + ) + assertThat(sutRenderAll.hideMediaContent(null, isMine = true)).isFalse() + assertThat(sutRenderAll.hideMediaContent(AN_EVENT_ID, isMine = true)).isFalse() + + val sutRenderOnly = aTimelineProtectionState( + protectionState = ProtectionState.RenderOnly(persistentSetOf()) + ) + assertThat(sutRenderOnly.hideMediaContent(null, isMine = true)).isFalse() + assertThat(sutRenderOnly.hideMediaContent(AN_EVENT_ID, isMine = true)).isFalse() + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt new file mode 100644 index 0000000000..06e436d64c --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultMessageSummaryFormatterTest.kt @@ -0,0 +1,104 @@ +/* + * 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.utils + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.features.location.api.Location +import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent.Mode +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent +import io.element.android.features.messages.impl.utils.messagesummary.DefaultMessageSummaryFormatter +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.timeline.aProfileDetails +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +class DefaultMessageSummaryFormatterTest : RobolectricTest() { + private val formatter = DefaultMessageSummaryFormatter( + RuntimeEnvironment.getApplication() as Context + ) + + @Test + @Config(qualifiers = "en") + fun `format call notification started`() { + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Started + ) + ) + assertThat(expected).isEqualTo("Call started") + } + + @Test + @Config(qualifiers = "en") + fun `format call notification declined by me`() { + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Declined(byMe = true) + ) + ) + assertThat(expected).isEqualTo("You declined a call") + } + + @Test + @Config(qualifiers = "en") + fun `format call notification declined`() { + val expected = formatter.format( + TimelineItemRtcNotificationContent( + callIntent = CallIntent.VIDEO, + state = RtcNotificationState.Declined(byMe = false) + ) + ) + assertThat(expected).isEqualTo("Call declined") + } + + @Test + @Config(qualifiers = "en") + fun `format live location`() { + val expected = formatter.format( + aLocationContent(isLive = true) + ) + assertThat(expected).isEqualTo("Shared live location") + } + + @Test + @Config(qualifiers = "en") + fun `format static location`() { + val expected = formatter.format( + aLocationContent(isLive = false) + ) + assertThat(expected).isEqualTo("Shared location") + } +} + +private fun aLocationContent(isLive: Boolean) = TimelineItemLocationContent( + senderId = A_USER_ID, + senderProfile = aProfileDetails(), + description = null, + assetType = null, + mode = if (isLive) { + Mode.Live( + lastKnownLocation = Location.fromGeoUri("geo:1,5"), + isActive = true, + endsAt = "", + endTimestamp = 0, + isOwnUser = true, + ) + } else { + Mode.Static( + location = Location.fromGeoUri("geo:1,5")!!, + ) + } +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt index 16c8c922ca..6390c2ba48 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/utils/DefaultTextPillificationHelperTest.kt @@ -9,7 +9,6 @@ package io.element.android.features.messages.impl.utils import android.net.Uri -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomAlias @@ -27,11 +26,10 @@ import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.libraries.textcomposer.mentions.getMentionSpans +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultTextPillificationHelperTest { +class DefaultTextPillificationHelperTest : RobolectricTest() { @Test fun `pillify - adds pills for user ids`() { val text = "A @user:server.com" diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts index b839f8de06..f89dd8de06 100644 --- a/features/messages/test/build.gradle.kts +++ b/features/messages/test/build.gradle.kts @@ -16,6 +16,7 @@ android { dependencies { api(projects.features.messages.impl) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.test) implementation(projects.libraries.audio.test) implementation(projects.libraries.mediaplayer.test) diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt index 02a6918ad8..fff3ede5d3 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/attachments/video/FakeMediaOptimizationSelectorPresenterFactory.kt @@ -26,7 +26,7 @@ class FakeMediaOptimizationSelectorPresenterFactory( ) } ) : MediaOptimizationSelectorPresenter.Factory { - override fun create(localMedia: LocalMedia): MediaOptimizationSelectorPresenter { + override fun create(localMedia: LocalMedia, sendAsFile: Boolean): MediaOptimizationSelectorPresenter { return fakePresenter } } diff --git a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt index 7b954abf86..591c31c7da 100644 --- a/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt +++ b/features/migration/impl/src/test/kotlin/io/element/android/features/migration/impl/migrations/AppMigration04Test.kt @@ -10,13 +10,11 @@ package io.element.android.features.migration.impl.migrations import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class AppMigration04Test { +class AppMigration04Test : RobolectricTest() { @Test fun `test migration`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context diff --git a/features/poll/impl/src/main/res/values-ca/translations.xml b/features/poll/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..21385e6473 --- /dev/null +++ b/features/poll/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,19 @@ + + + "Afegeix opció" + "Mostra els resultats només quan hagi finalitzat la votació" + "Amaga vots" + "Opció %1$d" + "Els canvis no s\'han desat. Segur que vols tornar enrere?" + "Pregunta o tema" + "De què tracta la votació?" + "Crea votació" + "Segur que vols eliminar la votació?" + "Elimina votació" + "Edita votació" + "No s\'han trobat votacions en curs." + "No s\'han trobat votacions passades." + "En curs" + "Pasades" + "Votacions" + diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt index 1ff25a0a81..ee6ad69ba6 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.poll.impl.history import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.poll.api.pollcontent.aPollContentState import io.element.android.features.poll.impl.R import io.element.android.features.poll.impl.history.model.PollHistoryFilter @@ -26,34 +28,28 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class PollHistoryViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class PollHistoryViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setPollHistoryViewView( + setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder ), goBack = it ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on edit poll invokes the expected callback`() { + fun `clicking on edit poll invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -69,17 +65,17 @@ class PollHistoryViewTest { eventSink = eventsRecorder ) ensureCalledOnceWithParam(eventId) { - rule.setPollHistoryViewView( + setPollHistoryViewView( state = state, onEditPoll = it ) - rule.clickOn(CommonStrings.action_edit_poll) + clickOn(CommonStrings.action_edit_poll) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on poll end emits the expected Event`() { + fun `clicking on poll end emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -95,16 +91,16 @@ class PollHistoryViewTest { ), eventSink = eventsRecorder ) - rule.setPollHistoryViewView( + setPollHistoryViewView( state = state, ) - rule.clickOn(CommonStrings.action_end_poll) + clickOn(CommonStrings.action_end_poll) // Cancel the dialog - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) // Do it again, and confirm the dialog - rule.clickOn(CommonStrings.action_end_poll) + clickOn(CommonStrings.action_end_poll) eventsRecorder.assertEmpty() - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle( PollHistoryEvents.EndPoll(eventId) ) @@ -112,7 +108,7 @@ class PollHistoryViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on poll answer emits the expected Event`() { + fun `clicking on poll answer emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( @@ -129,10 +125,10 @@ class PollHistoryViewTest { eventSink = eventsRecorder ) val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer - rule.setPollHistoryViewView( + setPollHistoryViewView( state = state, ) - rule.onNodeWithText( + onNodeWithText( text = answer.text, useUnmergedTree = true, ).performClick() @@ -142,14 +138,14 @@ class PollHistoryViewTest { } @Test - fun `clicking on past tab emits the expected Event`() { + fun `clicking on past tab emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPollHistoryViewView( + setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_polls_history_filter_past) + clickOn(R.string.screen_polls_history_filter_past) eventsRecorder.assertSingle( PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST) ) @@ -157,22 +153,22 @@ class PollHistoryViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on load more emits the expected Event`() { + fun `clicking on load more emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPollHistoryViewView( + setPollHistoryViewView( aPollHistoryState( hasMoreToLoad = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_load_more) + clickOn(CommonStrings.action_load_more) eventsRecorder.assertSingle( PollHistoryEvents.LoadMore ) } } -private fun AndroidComposeTestRule.setPollHistoryViewView( +private fun AndroidComposeUiTest.setPollHistoryViewView( state: PollHistoryState, onEditPoll: (EventId) -> Unit = EnsureNeverCalledWithParam(), goBack: () -> Unit = EnsureNeverCalled(), diff --git a/features/poll/test/build.gradle.kts b/features/poll/test/build.gradle.kts index a3779809d7..d0adc8e94f 100644 --- a/features/poll/test/build.gradle.kts +++ b/features/poll/test/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) api(projects.features.poll.api) implementation(libs.kotlinx.collections.immutable) diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index ad28c90966..1c801dbbad 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) + implementation(projects.libraries.cachestore.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) @@ -68,6 +69,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.fullscreenintent.api) implementation(projects.features.rageshake.api) @@ -114,6 +116,7 @@ dependencies { testImplementation(projects.features.logout.test) testImplementation(projects.libraries.indicator.test) testImplementation(projects.libraries.pushproviders.test) + testImplementation(projects.libraries.cachestore.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.services.appnavstate.impl) testImplementation(projects.services.analytics.test) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index b3fb68fe05..ef27c499af 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -20,4 +20,5 @@ sealed interface AdvancedSettingsEvents { data class SetTheme(val theme: ThemeOption) : AdvancedSettingsEvents data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents + data class SetLiveLocationMinimumDistanceUpdate(val value: Int) : AdvancedSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt index e58706e9fe..96b1cddc94 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsNode.kt @@ -10,12 +10,14 @@ package io.element.android.features.preferences.impl.advanced 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.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode +import io.element.android.libraries.androidutils.system.openAppSettingsPage import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -28,10 +30,12 @@ class AdvancedSettingsNode( @Composable override fun View(modifier: Modifier) { val state = presenter.present() + val context = LocalContext.current AdvancedSettingsView( state = state, modifier = modifier, - onBackClick = ::navigateUp + onBackClick = ::navigateUp, + onOpenAppSettingsClick = context::openAppSettingsPage ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index ae32bb01a5..45c136ab62 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -53,6 +53,10 @@ class AdvancedSettingsPresenter( appPreferencesStore.getThemeFlow().mapToTheme(isBlackThemeAllowed) }.collectAsState(initial = Theme.System) + val liveLocationMinimumDistanceUpdate by produceState(null) { + appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow().collect { value = it } + } + val mediaPreviewConfigState = mediaPreviewConfigStateStore.state() val themeOption by remember { @@ -117,6 +121,9 @@ class AdvancedSettingsPresenter( } is AdvancedSettingsEvents.SetHideInviteAvatars -> mediaPreviewConfigStateStore.setHideInviteAvatars(event.value) is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> mediaPreviewConfigStateStore.setTimelineMediaPreviewValue(event.value) + is AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate -> sessionCoroutineScope.launch { + appPreferencesStore.setLiveLocationMinimumDistanceInMetersUpdate(event.value) + } is AdvancedSettingsEvents.SetCompressImages -> sessionCoroutineScope.launch { sessionPreferencesStore.setOptimizeImages(event.compress) } @@ -133,6 +140,7 @@ class AdvancedSettingsPresenter( theme = themeOption, availableThemeOptions = availableThemeOptions, mediaPreviewConfigState = mediaPreviewConfigState, + liveLocationMinimumDistanceUpdate = liveLocationMinimumDistanceUpdate, eventSink = ::handleEvent, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 0525130048..94019c1f01 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -11,9 +11,9 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource +import io.element.android.features.preferences.impl.R import io.element.android.libraries.designsystem.components.preferences.DropdownOption import io.element.android.libraries.preferences.api.store.VideoCompressionPreset -import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList data class AdvancedSettingsState( @@ -23,6 +23,7 @@ data class AdvancedSettingsState( val theme: ThemeOption, val availableThemeOptions: ImmutableList, val mediaPreviewConfigState: MediaPreviewConfigState, + val liveLocationMinimumDistanceUpdate: Int?, val eventSink: (AdvancedSettingsEvents) -> Unit ) @@ -43,24 +44,24 @@ enum class ThemeOption : DropdownOption { System { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(CommonStrings.common_system) + override fun getText(): String = stringResource(R.string.theme_system) }, Light { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(CommonStrings.common_light) + override fun getText(): String = stringResource(R.string.theme_light) }, Dark { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(CommonStrings.common_dark) + override fun getText(): String = stringResource(R.string.theme_dark) }, Black { @Composable @ReadOnlyComposable - override fun getText(): String = stringResource(CommonStrings.common_black) + override fun getText(): String = stringResource(R.string.theme_black) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index 87df614074..2df59e3ced 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -41,6 +41,7 @@ fun aAdvancedSettingsState( availableThemeOptions: ImmutableList = ThemeOption.entries.toImmutableList(), hideInviteAvatars: Boolean = false, timelineMediaPreviewValue: MediaPreviewValue = MediaPreviewValue.On, + liveLocationMinimumDistanceUpdate: Int? = 50, setTimelineMediaPreviewAction: AsyncAction = AsyncAction.Uninitialized, setHideInviteAvatarsAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (AdvancedSettingsEvents) -> Unit = {}, @@ -56,5 +57,6 @@ fun aAdvancedSettingsState( setTimelineMediaPreviewAction = setTimelineMediaPreviewAction, setHideInviteAvatarsAction = setHideInviteAvatarsAction ), + liveLocationMinimumDistanceUpdate = liveLocationMinimumDistanceUpdate, eventSink = eventSink ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index 230f5fa739..c108c45d3b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -8,15 +8,24 @@ package io.element.android.features.preferences.impl.advanced +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import im.vector.app.features.analytics.plan.Interaction import io.element.android.compound.theme.ElementTheme import io.element.android.features.preferences.impl.R @@ -33,10 +42,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.stringWithLink import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.ListSupportingText import io.element.android.libraries.designsystem.theme.components.ListSupportingTextDefaults +import io.element.android.libraries.designsystem.theme.components.Slider import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost @@ -47,11 +58,13 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.compose.LocalAnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction +import kotlin.math.roundToInt @Composable fun AdvancedSettingsView( state: AdvancedSettingsState, onBackClick: () -> Unit, + onOpenAppSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { val analyticsService = LocalAnalyticsService.current @@ -190,6 +203,15 @@ fun AdvancedSettingsView( } ModerationAndSafety(state) + if (state.liveLocationMinimumDistanceUpdate != null) { + LiveLocationUpdatesSection( + value = state.liveLocationMinimumDistanceUpdate, + onSaveValue = { value -> + state.eventSink(AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate(value)) + }, + onOpenAppPermissionsClick = onOpenAppSettingsClick, + ) + } } } @@ -314,6 +336,78 @@ private fun ModerationAndSafety( } } +@Composable +private fun LiveLocationUpdatesSection( + value: Int, + onSaveValue: (Int) -> Unit, + onOpenAppPermissionsClick: () -> Unit, + modifier: Modifier = Modifier, +) { + PreferenceCategory( + modifier = modifier, + showTopDivider = true, + ) { + ListSectionHeader( + title = stringResource(R.string.screen_advanced_settings_live_location_section_title), + description = { + ListSupportingText( + text = stringResource(R.string.screen_advanced_settings_live_location_section_description), + contentPadding = ListSupportingTextDefaults.Padding.None, + ) + } + ) + var sliderValue by remember(value) { mutableIntStateOf(value) } + Column( + modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = pluralStringResource( + R.plurals.screen_advanced_settings_live_location_update_distance, + sliderValue, + sliderValue, + ), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + ) + val valueRange = 1f..100f + val start = valueRange.start.toInt() + val end = valueRange.endInclusive.toInt() + Row(verticalAlignment = Alignment.CenterVertically) { + Text("${start}m", color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular) + Slider( + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp), + value = sliderValue.toFloat(), + onValueChange = { sliderValue = it.roundToInt() }, + onValueChangeFinish = { + onSaveValue(sliderValue) + }, + valueRange = valueRange, + colors = SliderDefaults.colors( + thumbColor = ElementTheme.colors.iconAccentPrimary, + activeTrackColor = ElementTheme.colors.iconAccentPrimary, + inactiveTrackColor = ElementTheme.colors.bgBadgeAccent, + inactiveTickColor = ElementTheme.colors.iconAccentPrimary, + ) + ) + Text("${end}m", color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodyMdRegular) + } + } + val footerText = stringWithLink( + textRes = R.string.screen_advanced_settings_live_location_section_footer, + url = "", + linkTextRes = R.string.screen_advanced_settings_live_location_section_footer_link, + onLinkClick = { onOpenAppPermissionsClick() }, + ) + ListSupportingText( + annotatedString = footerText, + contentPadding = ListSupportingTextDefaults.Padding.Default, + ) + } +} + @PreviewWithLargeHeight @Composable internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = @@ -334,7 +428,8 @@ internal fun AdvancedSettingsViewBlackPreview(@PreviewParameter(AdvancedSettings private fun ContentToPreview(state: AdvancedSettingsState) { AdvancedSettingsView( state = state, - onBackClick = { } + onBackClick = { }, + onOpenAppSettingsClick = {} ) } 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 index 4c76c6ec7e..e7ad6e2284 100644 --- 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 @@ -25,10 +25,7 @@ import io.element.android.features.rageshake.api.preferences.RageshakePreference 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 @@ -45,7 +42,6 @@ class AppDeveloperSettingsPresenter( private val featureFlagService: FeatureFlagService, private val rageshakePresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, - private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): AppDeveloperSettingsState { @@ -71,14 +67,6 @@ class AppDeveloperSettingsPresenter( 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))) } 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 index 71051cf829..9b2a2aa3a9 100644 --- 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 @@ -87,14 +87,6 @@ fun AppDeveloperSettingsView( onClick = onOpenShowkase ) } - PreferenceCategory(title = "Crash") { - ListItem( - headlineContent = { - Text("Crash the app 💥") - }, - onClick = { error("This crash is a test.") } - ) - } RageshakePreferencesView( state = state.rageshakeState, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt index 8f1b332c8c..5ca7fc9fdf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt @@ -8,6 +8,8 @@ package io.element.android.features.preferences.impl.notifications +import io.element.android.libraries.preferences.api.store.NotificationSound + sealed interface NotificationSettingsEvents { data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents @@ -20,4 +22,16 @@ sealed interface NotificationSettingsEvents { data object ChangePushProvider : NotificationSettingsEvents data object CancelChangePushProvider : NotificationSettingsEvents data class SetPushProvider(val index: Int) : NotificationSettingsEvents + data class SetMessageSound(val sound: NotificationSound) : NotificationSettingsEvents + data class SetCallRingtone(val sound: NotificationSound) : NotificationSettingsEvents + data object DismissMessageSoundCopyError : NotificationSettingsEvents + data object DismissCallRingtoneCopyError : NotificationSettingsEvents + data object ShowMessageSoundDialog : NotificationSettingsEvents + data object DismissMessageSoundDialog : NotificationSettingsEvents + data class SelectMessageSoundPreset(val sound: NotificationSound) : NotificationSettingsEvents + data object LaunchMessageSoundPicker : NotificationSettingsEvents + data object ShowCallRingtoneDialog : NotificationSettingsEvents + data object DismissCallRingtoneDialog : NotificationSettingsEvents + data class SelectCallRingtonePreset(val sound: NotificationSound) : NotificationSettingsEvents + data object LaunchCallRingtonePicker : NotificationSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 9d9e80b3f3..03fe2cf31b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -16,10 +16,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf 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 dev.zacsweers.metro.Inject +import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -30,18 +32,30 @@ import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermiss import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.NotificationSound import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.api.notifications.NotificationSoundUpdater +import io.element.android.libraries.push.api.notifications.SoundDisplayNameResolver +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier.CopyResult +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier.SoundSlot import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber import kotlin.time.Duration.Companion.seconds @Inject @@ -52,9 +66,22 @@ class NotificationSettingsPresenter( private val pushService: PushService, private val systemNotificationsEnabledProvider: SystemNotificationsEnabledProvider, private val fullScreenIntentPermissionsPresenter: Presenter, + private val appPreferencesStore: AppPreferencesStore, + private val notificationSoundUpdater: NotificationSoundUpdater, + private val notificationSoundCopier: NotificationSoundCopier, + private val soundDisplayNameResolver: SoundDisplayNameResolver, + private val stringProvider: StringProvider, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : Presenter { + // Serializes the pick → copy → persist → recreate pipeline per slot. The copier already locks + // its `.tmp` file, but the persist+recreate window outside the copier is unprotected; + // without this, two rapid picks (e.g. an automation or accessibility service firing back-to- + // back events) could interleave and leave the channel id pointing at the older version while + // DataStore reflects the newer one. + private val messageSoundLock = Mutex() + private val callRingtoneLock = Mutex() + @Composable override fun present(): NotificationSettingsState { val userPushStore = remember { userPushStoreFactory.getOrCreate(matrixClient.sessionId) } @@ -109,6 +136,42 @@ class NotificationSettingsPresenter( var showChangePushProviderDialog by remember { mutableStateOf(false) } + val messageSound by remember { appPreferencesStore.getMessageSoundFlow() }.collectAsState(initial = NotificationSound.SystemDefault) + val callRingtone by remember { appPreferencesStore.getCallRingtoneFlow() }.collectAsState(initial = NotificationSound.SystemDefault) + val persistedMessageSoundTitle by remember { appPreferencesStore.getMessageSoundDisplayNameFlow() }.collectAsState(initial = null) + val persistedCallRingtoneTitle by remember { appPreferencesStore.getCallRingtoneDisplayNameFlow() }.collectAsState(initial = null) + val defaultLabel = stringProvider.getString(R.string.screen_notification_settings_sound_system_default) + + // One-shot classification for the call-ringtone slot only: users who customised the + // channel via Android system settings before the in-app picker existed (channel version 0, + // persisted SystemDefault) get reflected here. The message-sound equivalent is handled by + // the eager migration in DefaultNotificationChannels.createNotificationChannels(). Cleared + // on any in-app pick so a stale read can't bleed into the row after the recreate. + var legacyCallRingtone by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + val config = appPreferencesStore.getNotificationSoundChannelConfig() + if (config.callRingtone == NotificationSound.SystemDefault && config.callRingtoneVersion == 0) { + legacyCallRingtone = notificationSoundUpdater.readRingingCallChannelSound() + } + } + + val effectiveMessageSound = messageSound + val effectiveCallRingtone = if (callRingtone == NotificationSound.SystemDefault) { + legacyCallRingtone ?: callRingtone + } else { + callRingtone + } + + val messageSoundDisplayName = probeSoundDisplayName(effectiveMessageSound, persistedMessageSoundTitle, defaultLabel) + val callRingtoneDisplayName = probeSoundDisplayName(effectiveCallRingtone, persistedCallRingtoneTitle, defaultLabel) + + var messageSoundCopyError by remember { mutableStateOf(false) } + var callRingtoneCopyError by remember { mutableStateOf(false) } + var showMessageSoundDialog by remember { mutableStateOf(false) } + var pendingMessageSoundPickerLaunch by remember { mutableIntStateOf(0) } + var showCallRingtoneDialog by remember { mutableStateOf(false) } + var pendingCallRingtonePickerLaunch by remember { mutableIntStateOf(0) } + fun CoroutineScope.changePushProvider( data: Pair? ) = launch { @@ -157,6 +220,62 @@ class NotificationSettingsPresenter( NotificationSettingsEvents.ChangePushProvider -> showChangePushProviderDialog = true NotificationSettingsEvents.CancelChangePushProvider -> showChangePushProviderDialog = false is NotificationSettingsEvents.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index)) + is NotificationSettingsEvents.SetMessageSound -> applyMessageSound( + sound = event.sound, + sessionCoroutineScope = sessionCoroutineScope, + onCopyError = { messageSoundCopyError = true }, + onCopySuccess = { messageSoundCopyError = false }, + onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, + ) + is NotificationSettingsEvents.SelectMessageSoundPreset -> { + showMessageSoundDialog = false + applyMessageSound( + sound = event.sound, + sessionCoroutineScope = sessionCoroutineScope, + onCopyError = { messageSoundCopyError = true }, + onCopySuccess = { messageSoundCopyError = false }, + onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, + ) + } + NotificationSettingsEvents.ShowMessageSoundDialog -> showMessageSoundDialog = true + NotificationSettingsEvents.DismissMessageSoundDialog -> showMessageSoundDialog = false + NotificationSettingsEvents.LaunchMessageSoundPicker -> { + showMessageSoundDialog = false + pendingMessageSoundPickerLaunch++ + } + is NotificationSettingsEvents.SetCallRingtone -> { + legacyCallRingtone = null + applyCallRingtone( + sound = event.sound, + sessionCoroutineScope = sessionCoroutineScope, + onCopyError = { callRingtoneCopyError = true }, + onCopySuccess = { callRingtoneCopyError = false }, + onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, + ) + } + is NotificationSettingsEvents.SelectCallRingtonePreset -> { + showCallRingtoneDialog = false + legacyCallRingtone = null + applyCallRingtone( + sound = event.sound, + sessionCoroutineScope = sessionCoroutineScope, + onCopyError = { callRingtoneCopyError = true }, + onCopySuccess = { callRingtoneCopyError = false }, + onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, + ) + } + NotificationSettingsEvents.ShowCallRingtoneDialog -> showCallRingtoneDialog = true + NotificationSettingsEvents.DismissCallRingtoneDialog -> showCallRingtoneDialog = false + NotificationSettingsEvents.LaunchCallRingtonePicker -> { + showCallRingtoneDialog = false + pendingCallRingtonePickerLaunch++ + } + NotificationSettingsEvents.DismissMessageSoundCopyError -> { + messageSoundCopyError = false + } + NotificationSettingsEvents.DismissCallRingtoneCopyError -> { + callRingtoneCopyError = false + } } } @@ -171,10 +290,140 @@ class NotificationSettingsPresenter( availablePushDistributors = availableDistributors, showChangePushProviderDialog = showChangePushProviderDialog, fullScreenIntentPermissionsState = key(refreshFullScreenIntentSettings) { fullScreenIntentPermissionsPresenter.present() }, + messageSound = NotificationSettingsState.SoundChannelUiState( + sound = effectiveMessageSound, + displayName = messageSoundDisplayName, + copyError = messageSoundCopyError, + ), + callRingtone = NotificationSettingsState.SoundChannelUiState( + sound = effectiveCallRingtone, + displayName = callRingtoneDisplayName, + copyError = callRingtoneCopyError, + ), + showMessageSoundDialog = showMessageSoundDialog, + showCallRingtoneDialog = showCallRingtoneDialog, + pendingMessageSoundPickerLaunch = pendingMessageSoundPickerLaunch, + pendingCallRingtonePickerLaunch = pendingCallRingtonePickerLaunch, eventSink = ::handleEvent, ) } + private fun applyMessageSound( + sound: NotificationSound, + sessionCoroutineScope: CoroutineScope, + onCopyError: () -> Unit, + onCopySuccess: () -> Unit, + onChannelFailure: (Throwable) -> Unit, + ) { + sessionCoroutineScope.launch { + messageSoundLock.withLock { + // Re-selecting the current non-Custom sound is a no-op: skip the channel churn + // (delete + create round-trips through system_server) and the version bump. Custom + // still falls through so the copier refreshes the file from the (possibly updated) + // source URI. + if (sound !is NotificationSound.Custom && appPreferencesStore.getMessageSoundFlow().first() == sound) { + onCopySuccess() + return@withLock + } + val resolved = resolvePickedSound(sound, SoundSlot.Message, onCopyError) ?: return@withLock + runCatchingExceptions { + onCopySuccess() + val newVersion = appPreferencesStore.setMessageSoundAndIncrementVersion(resolved.first, resolved.second) + notificationSoundUpdater.recreateNoisyChannel(resolved.first, newVersion) + // Non-Custom picks bypass the copier, so they don't sweep the prior Custom file + // inline. Drop it now that the new channel no longer references it. + if (resolved.first !is NotificationSound.Custom) { + notificationSoundCopier.deleteStoredSoundFor(SoundSlot.Message) + } + }.onFailure(onChannelFailure) + } + } + } + + private fun applyCallRingtone( + sound: NotificationSound, + sessionCoroutineScope: CoroutineScope, + onCopyError: () -> Unit, + onCopySuccess: () -> Unit, + onChannelFailure: (Throwable) -> Unit, + ) { + sessionCoroutineScope.launch { + callRingtoneLock.withLock { + if (sound !is NotificationSound.Custom && appPreferencesStore.getCallRingtoneFlow().first() == sound) { + onCopySuccess() + return@withLock + } + val resolved = resolvePickedSound(sound, SoundSlot.Call, onCopyError) ?: return@withLock + runCatchingExceptions { + onCopySuccess() + val newVersion = appPreferencesStore.setCallRingtoneAndIncrementVersion(resolved.first, resolved.second) + notificationSoundUpdater.recreateRingingCallChannel(resolved.first, newVersion) + if (resolved.first !is NotificationSound.Custom) { + notificationSoundCopier.deleteStoredSoundFor(SoundSlot.Call) + } + }.onFailure(onChannelFailure) + } + } + } + + /** + * Custom rows prefer [persistedTitle] (captured at copy time), fall back to a live resolver + * probe for legacy data, then to a localised "Custom" — never to [defaultLabel], so an + * unresolvable Custom isn't mislabelled as Default. + */ + @Composable + private fun probeSoundDisplayName( + sound: NotificationSound, + persistedTitle: String?, + defaultLabel: String, + ): String = when (sound) { + NotificationSound.SystemDefault -> defaultLabel + NotificationSound.ElementDefault -> stringProvider.getString(R.string.screen_notification_settings_sound_element_default) + NotificationSound.ElementFade -> stringProvider.getString(R.string.screen_notification_settings_sound_element_fade) + NotificationSound.Silent -> stringProvider.getString(R.string.screen_notification_settings_sound_silent) + is NotificationSound.Custom -> { + val nonBlankPersisted = persistedTitle?.takeUnless { it.isBlank() } + if (nonBlankPersisted != null) { + nonBlankPersisted + } else { + val customFallback = stringProvider.getString(R.string.screen_notification_settings_sound_custom_fallback) + val resolved by produceState(initialValue = "", sound.uri) { + val title = soundDisplayNameResolver.resolveCustomSoundTitle(sound.uri) + value = title?.takeUnless { it.isBlank() } ?: customFallback + } + resolved + } + } + } + + /** + * For a Custom pick, copies into app-private storage and returns (FileProvider URI, title). + * Returns null on copy failure after invoking [onCopyError]. Pass-through for SystemDefault / + * Silent. + */ + private suspend fun resolvePickedSound( + requested: NotificationSound, + slot: SoundSlot, + onCopyError: () -> Unit, + ): Pair? { + if (requested !is NotificationSound.Custom) return requested to null + return when (val result = notificationSoundCopier.copyToAppFiles(requested.uri, slot)) { + is CopyResult.Success -> NotificationSound.Custom(result.fileProviderUriString) to result.displayName + is CopyResult.Failure -> { + Timber.w(result.cause, "Notification sound copy failed for slot=%s", slot) + onCopyError() + null + } + CopyResult.UnplayableSource, + CopyResult.UnplayableCopy, + CopyResult.FileTooLarge -> { + Timber.w("Notification sound rejected: result=%s slot=%s", result::class.simpleName, slot) + onCopyError() + null + } + } + } + @OptIn(FlowPreview::class) private fun CoroutineScope.observeNotificationSettings( target: MutableState, @@ -223,7 +472,7 @@ class NotificationSettingsPresenter( notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = encryptedGroupDefaultMode != RoomNotificationMode.ALL_MESSAGES, mode = RoomNotificationMode.ALL_MESSAGES, - isOneToOne = false, + isDM = false, ) } @@ -234,7 +483,7 @@ class NotificationSettingsPresenter( notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = encryptedOneToOneDefaultMode != RoomNotificationMode.ALL_MESSAGES, mode = RoomNotificationMode.ALL_MESSAGES, - isOneToOne = true, + isDM = true, ) } }.fold( diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index 0a55909e2c..3c59f6c355 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.preferences.api.store.NotificationSound import io.element.android.libraries.pushproviders.api.Distributor import kotlinx.collections.immutable.ImmutableList @@ -23,6 +24,24 @@ data class NotificationSettingsState( val availablePushDistributors: ImmutableList, val showChangePushProviderDialog: Boolean, val fullScreenIntentPermissionsState: FullScreenIntentPermissionsState, + val messageSound: SoundChannelUiState, + val callRingtone: SoundChannelUiState, + val showMessageSoundDialog: Boolean, + val showCallRingtoneDialog: Boolean, + /** + * One-shot trigger for launching the system ringtone picker. Each + * [NotificationSettingsEvents.LaunchMessageSoundPicker] increments the value; + * the view watches this in `LaunchedEffect` and calls the launcher whenever it goes above 0. + * **Always start at 0** in initial state, providers, and previews — a non-zero seed will + * auto-open the picker on screen entry. + */ + val pendingMessageSoundPickerLaunch: Int, + /** + * One-shot trigger for launching the system ringtone picker from the call ringtone dialog. + * Same contract as [pendingMessageSoundPickerLaunch]: **always start at 0**, or the picker + * auto-opens on screen entry. + */ + val pendingCallRingtonePickerLaunch: Int, val eventSink: (NotificationSettingsEvents) -> Unit, ) { sealed interface MatrixSettings { @@ -45,6 +64,16 @@ data class NotificationSettingsState( val appNotificationsEnabled: Boolean, ) + /** + * UI state for one of the two sound rows (message sound, call ringtone). [copyError] is set + * when the last pick failed to copy into app-private storage; the persisted choice stays put. + */ + data class SoundChannelUiState( + val sound: NotificationSound, + val displayName: String, + val copyError: Boolean, + ) + /** * Whether the advanced settings should be shown. * This is true if the current push distributor is in a failure state or if there are multiple push distributors available. diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt index fb396443c3..905f869dbe 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.preferences.api.store.NotificationSound import io.element.android.libraries.pushproviders.api.Distributor import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -45,6 +46,34 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider Unit = {}, ) = NotificationSettingsState( matrixSettings = NotificationSettingsState.MatrixSettings.Valid( @@ -80,6 +119,20 @@ fun aValidNotificationSettingsState( availablePushDistributors = availablePushDistributors.toImmutableList(), showChangePushProviderDialog = showChangePushProviderDialog, fullScreenIntentPermissionsState = fullScreenIntentPermissionsState, + messageSound = NotificationSettingsState.SoundChannelUiState( + sound = messageSound, + displayName = messageSoundDisplayName, + copyError = messageSoundCopyError, + ), + callRingtone = NotificationSettingsState.SoundChannelUiState( + sound = callRingtone, + displayName = callRingtoneDisplayName, + copyError = callRingtoneCopyError, + ), + showMessageSoundDialog = showMessageSoundDialog, + showCallRingtoneDialog = showCallRingtoneDialog, + pendingMessageSoundPickerLaunch = pendingMessageSoundPickerLaunch, + pendingCallRingtonePickerLaunch = pendingCallRingtonePickerLaunch, eventSink = eventSink, ) @@ -99,6 +152,20 @@ fun aInvalidNotificationSettingsState( availablePushDistributors = persistentListOf(), showChangePushProviderDialog = false, fullScreenIntentPermissionsState = aFullScreenIntentPermissionsState(), + messageSound = NotificationSettingsState.SoundChannelUiState( + sound = NotificationSound.SystemDefault, + displayName = "System default", + copyError = false, + ), + callRingtone = NotificationSettingsState.SoundChannelUiState( + sound = NotificationSound.SystemDefault, + displayName = "System default", + copyError = false, + ), + showMessageSoundDialog = false, + showCallRingtoneDialog = false, + pendingMessageSoundPickerLaunch = 0, + pendingCallRingtonePickerLaunch = 0, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 09f54b7027..2009970448 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -8,16 +8,29 @@ package io.element.android.features.preferences.impl.notifications +import android.app.Activity +import android.media.RingtoneManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.preferences.impl.R import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent @@ -35,13 +48,17 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen 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.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.ListItem import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.preferences.api.store.NotificationSound import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList /** @@ -182,6 +199,7 @@ private fun NotificationSettingsContentView( onCheckedChange = onMentionNotificationsChange ) } + SoundsPreferenceCategory(state = state) PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_additional_settings_section_title)) { // TODO We are removing the call notification toggle until support for call notifications has been added // PreferenceSwitch( @@ -261,6 +279,189 @@ private fun NotificationSettingsContentView( } } +@Composable +private fun SoundsPreferenceCategory(state: NotificationSettingsState) { + PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_sound_section_title)) { + val launchMessageSoundPicker = rememberSoundPickerOnClick( + type = RingtoneManager.TYPE_NOTIFICATION, + current = state.messageSound.sound, + defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI, + onSoundPick = { sound -> state.eventSink(NotificationSettingsEvents.SetMessageSound(sound)) }, + ) + // Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only + // increments fired by LaunchMessageSoundPicker should launch it. + LaunchedEffect(state.pendingMessageSoundPickerLaunch) { + if (state.pendingMessageSoundPickerLaunch > 0) { + launchMessageSoundPicker() + } + } + ListItem( + headlineContent = { Text(stringResource(id = R.string.screen_notification_settings_message_sound_label)) }, + supportingContent = { Text(state.messageSound.displayName) }, + onClick = { state.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) }, + ) + if (state.messageSound.copyError) { + SoundCopyErrorRow( + onDismissClick = { state.eventSink(NotificationSettingsEvents.DismissMessageSoundCopyError) }, + ) + } + if (state.showMessageSoundDialog) { + MessageSoundDialog(state) + } + + val launchCallRingtonePicker = rememberSoundPickerOnClick( + type = RingtoneManager.TYPE_RINGTONE, + current = state.callRingtone.sound, + defaultUri = Settings.System.DEFAULT_RINGTONE_URI, + onSoundPick = { sound -> state.eventSink(NotificationSettingsEvents.SetCallRingtone(sound)) }, + ) + // Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only + // increments fired by LaunchCallRingtonePicker should launch it. + LaunchedEffect(state.pendingCallRingtonePickerLaunch) { + if (state.pendingCallRingtonePickerLaunch > 0) { + launchCallRingtonePicker() + } + } + ListItem( + headlineContent = { Text(stringResource(id = R.string.screen_notification_settings_call_ringtone_label)) }, + supportingContent = { Text(state.callRingtone.displayName) }, + onClick = { state.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) }, + ) + if (state.callRingtone.copyError) { + SoundCopyErrorRow( + onDismissClick = { state.eventSink(NotificationSettingsEvents.DismissCallRingtoneCopyError) }, + ) + } + if (state.showCallRingtoneDialog) { + CallRingtoneDialog(state) + } + } +} + +@Composable +private fun MessageSoundDialog(state: NotificationSettingsState) { + val initialSelection = when (state.messageSound.sound) { + NotificationSound.ElementDefault -> 0 + NotificationSound.ElementFade -> 1 + NotificationSound.SystemDefault -> 2 + else -> null + } + // When the user is on Custom or Silent, no preset row is selected — surface the current + // sound as a subtitle so screen readers (and sighted users) still have state context. + val subtitle = if (initialSelection == null) { + stringResource( + id = R.string.screen_notification_settings_message_sound_dialog_current_subtitle, + state.messageSound.displayName, + ) + } else { + null + } + SingleSelectionDialog( + title = stringResource(id = R.string.screen_notification_settings_message_sound_dialog_title), + subtitle = subtitle, + options = persistentListOf( + ListOption(title = stringResource(id = R.string.screen_notification_settings_sound_element_default)), + ListOption(title = stringResource(id = R.string.screen_notification_settings_sound_element_fade)), + ListOption(title = stringResource(id = R.string.screen_notification_settings_sound_system_default)), + ListOption(title = stringResource(id = R.string.screen_notification_settings_message_sound_dialog_choose_other)), + ), + initialSelection = initialSelection, + onSelectOption = { index -> + when (index) { + 0 -> state.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementDefault)) + 1 -> state.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementFade)) + 2 -> state.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.SystemDefault)) + else -> state.eventSink(NotificationSettingsEvents.LaunchMessageSoundPicker) + } + }, + onDismissRequest = { state.eventSink(NotificationSettingsEvents.DismissMessageSoundDialog) }, + ) +} + +@Composable +private fun CallRingtoneDialog(state: NotificationSettingsState) { + val initialSelection = when (state.callRingtone.sound) { + NotificationSound.SystemDefault -> 0 + else -> null + } + val subtitle = if (initialSelection == null) { + stringResource( + id = R.string.screen_notification_settings_message_sound_dialog_current_subtitle, + state.callRingtone.displayName, + ) + } else { + null + } + SingleSelectionDialog( + title = stringResource(id = R.string.screen_notification_settings_call_ringtone_label), + subtitle = subtitle, + options = persistentListOf( + ListOption(title = stringResource(id = R.string.screen_notification_settings_sound_system_default)), + ListOption(title = stringResource(id = R.string.screen_notification_settings_message_sound_dialog_choose_other)), + ), + initialSelection = initialSelection, + onSelectOption = { index -> + when (index) { + 0 -> state.eventSink(NotificationSettingsEvents.SelectCallRingtonePreset(NotificationSound.SystemDefault)) + else -> state.eventSink(NotificationSettingsEvents.LaunchCallRingtonePicker) + } + }, + onDismissRequest = { state.eventSink(NotificationSettingsEvents.DismissCallRingtoneDialog) }, + ) +} + +@Composable +private fun SoundCopyErrorRow( + onDismissClick: () -> Unit, +) { + ListItem( + modifier = Modifier + .background(ElementTheme.colors.bgSubtleSecondary) + .semantics { liveRegion = LiveRegionMode.Polite }, + headlineContent = { + Text( + text = stringResource(R.string.screen_notification_settings_sound_set_sound_error_title), + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + }, + trailingContent = ListItemContent.Custom { _ -> + IconButton(onClick = onDismissClick) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(R.string.screen_notification_settings_sound_set_sound_error_dismiss_a11y), + ) + } + }, + ) +} + +@Composable +private fun rememberSoundPickerOnClick( + type: Int, + current: NotificationSound, + defaultUri: Uri, + onSoundPick: (NotificationSound) -> Unit, +): () -> Unit { + // Paparazzi previews don't provide a LocalActivityResultRegistryOwner, which + // rememberLauncherForActivityResult requires. Skip the launcher in inspection mode and + // return a no-op click handler — previews don't need to launch the picker. + if (LocalInspectionMode.current) return {} + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val sound = result.data?.toPickedNotificationSound(defaultUri) + if (sound != null) { + onSoundPick(sound) + } + } + } + return { + launcher.launch(buildRingtonePickerIntent(type = type, current = current, defaultUri = defaultUri)) + } +} + @Composable private fun getTitleForRoomNotificationMode(mode: RoomNotificationMode?) = when (mode) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundPicker.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundPicker.kt new file mode 100644 index 0000000000..df9d8bb0c4 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundPicker.kt @@ -0,0 +1,55 @@ +/* + * 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.notifications + +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import androidx.core.content.IntentCompat +import androidx.core.net.toUri +import io.element.android.libraries.preferences.api.store.NotificationSound + +/** + * Builds an `ACTION_RINGTONE_PICKER` intent for [type] (notification or ringtone), pre-selecting + * the user's [current] choice and showing the system [defaultUri] as the "Default" option. + */ +internal fun buildRingtonePickerIntent( + type: Int, + current: NotificationSound, + defaultUri: Uri, +): Intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, type) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) + putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true) + putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri) + val existingUri: Uri? = when (current) { + // ElementDefault and ElementFade refer to bundled raw resources, which the system picker + // can't enumerate. Fall back to the picker's "Default" row so the user has something selected. + NotificationSound.SystemDefault, + NotificationSound.ElementDefault, + NotificationSound.ElementFade -> defaultUri + NotificationSound.Silent -> null + is NotificationSound.Custom -> current.uri.toUri() + } + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingUri) +} + +/** + * Maps the URI returned by the ringtone picker into a [NotificationSound]: + * - null URI → the user picked "Silent", + * - URI matching [defaultUri] → the user picked "Default", + * - any other URI → the user picked a specific ringtone. + */ +internal fun Intent.toPickedNotificationSound(defaultUri: Uri): NotificationSound { + val pickedUri: Uri? = IntentCompat.getParcelableExtra(this, RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) + return when { + pickedUri == null -> NotificationSound.Silent + pickedUri == defaultUri -> NotificationSound.SystemDefault + else -> NotificationSound.Custom(pickedUri.toString()) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt index 96097983c9..1357caeef3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt @@ -34,12 +34,12 @@ class EditDefaultNotificationSettingNode( } data class Inputs( - val isOneToOne: Boolean + val isDm: Boolean ) : NodeInputs private val callback: Callback = callback() private val inputs = inputs() - private val presenter = presenterFactory.create(inputs.isOneToOne) + private val presenter = presenterFactory.create(inputs.isDm) @Composable override fun View(modifier: Modifier) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index 178f7033f3..f2cb8b02cc 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -42,12 +42,12 @@ import kotlin.time.Duration.Companion.seconds @AssistedInject class EditDefaultNotificationSettingPresenter( private val notificationSettingsService: NotificationSettingsService, - @Assisted private val isOneToOne: Boolean, + @Assisted private val isDm: Boolean, private val roomListService: RoomListService, ) : Presenter { @AssistedFactory interface Factory { - fun create(isOneToOne: Boolean): EditDefaultNotificationSettingPresenter + fun create(isDm: Boolean): EditDefaultNotificationSettingPresenter } private val collator = Collator.getInstance().apply { @@ -86,7 +86,7 @@ class EditDefaultNotificationSettingPresenter( } return EditDefaultNotificationSettingState( - isOneToOne = isOneToOne, + isOneToOne = isDm, mode = mode.value, roomsWithUserDefinedMode = roomsWithUserDefinedMode.value.toImmutableList(), changeNotificationSettingAction = changeNotificationSettingAction.value, @@ -96,7 +96,7 @@ class EditDefaultNotificationSettingPresenter( } private fun CoroutineScope.fetchSettings(mode: MutableState) = launch { - mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isOneToOne).getOrThrow() + mode.value = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = isDm).getOrThrow() } @OptIn(FlowPreview::class) @@ -129,7 +129,7 @@ class EditDefaultNotificationSettingPresenter( val roomWithUserDefinedRules: Set = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet() roomsWithUserDefinedMode.value = summaries .filter { roomSummary -> - roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isOneToOne == isOneToOne + roomWithUserDefinedRules.contains(roomSummary.roomId) && roomSummary.isDm == isDm } .map { roomSummary -> EditNotificationSettingRoomInfo( @@ -154,9 +154,9 @@ class EditDefaultNotificationSettingPresenter( private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState>) = launch { action.runUpdatingStateNoSuccess { // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isDM = isDm) .map { - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isDM = isDm) } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 43da681a37..f407149007 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.rageshake.api.RageshakeFeatureAvailability @@ -55,6 +56,7 @@ class PreferencesRootPresenter( private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val featureFlagService: FeatureFlagService, private val sessionStore: SessionStore, + private val sessionEnterpriseService: SessionEnterpriseService, ) : Presenter { @Composable override fun present(): PreferencesRootState { @@ -158,6 +160,10 @@ class PreferencesRootPresenter( private fun CoroutineScope.initAccountManagementUrl( accountManagementUrl: MutableState, ) = launch { - accountManagementUrl.value = matrixClient.getAccountManagementUrl(null).getOrNull() + accountManagementUrl.value = matrixClient.getAccountManagementUrl(null) + .getOrNull() + ?.let { + sessionEnterpriseService.tweakMasUrl(it) + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt index 6c26866e93..f3faf96ba2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -11,9 +11,9 @@ package io.element.android.features.preferences.impl.tasks import android.content.Context import coil3.SingletonImageLoader import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Provider import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.preferences.impl.DefaultCacheService +import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.ApplicationContext @@ -33,12 +33,15 @@ class DefaultClearCacheUseCase( private val matrixClient: MatrixClient, private val coroutineDispatchers: CoroutineDispatchers, private val defaultCacheService: DefaultCacheService, - private val okHttpClient: Provider, + private val okHttpClient: () -> OkHttpClient, private val pushService: PushService, private val seenInvitesStore: SeenInvitesStore, private val activeRoomsHolder: ActiveRoomsHolder, + private val cacheStore: CacheStore, ) : ClearCacheUseCase { override suspend fun invoke() = withContext(coroutineDispatchers.io) { + // Clear cache store + cacheStore.deleteAll() // Active rooms should be disposed of before clearing the cache activeRoomsHolder.clear(matrixClient.sessionId) // Clear Matrix cache @@ -51,7 +54,12 @@ class DefaultClearCacheUseCase( // Clear OkHttp cache okHttpClient().cache?.delete() // Clear app cache - context.cacheDir.deleteRecursively() + context.cacheDir?.listFiles { + // But keep the logs + it.name != "logs" + }?.onEach { + it.deleteRecursively() + } // Clear some settings seenInvitesStore.clear() // Ensure any error will be displayed again 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 13f69a7e1e..2f238f9935 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 @@ -10,6 +10,7 @@ package io.element.android.features.preferences.impl.user.editprofile import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.preview.USER_NAME_JOHN_DOE import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.permissions.api.PermissionsState @@ -28,7 +29,7 @@ open class EditUserProfileStateProvider : PreviewParameterProvider = emptyList(), saveButtonEnabled: Boolean = true, diff --git a/features/preferences/impl/src/main/res/values-be/translations.xml b/features/preferences/impl/src/main/res/values-be/translations.xml index b27a22bb8d..7f487afa24 100644 --- a/features/preferences/impl/src/main/res/values-be/translations.xml +++ b/features/preferences/impl/src/main/res/values-be/translations.xml @@ -53,6 +53,9 @@ "налады сістэмы" "Сістэмныя апавяшчэнні выключаны" "Апавяшчэнні" + "Цёмная" + "Светлая" + "Сістэмная" "Выпраўленне непаладак" "Выпраўленне непаладак з апавяшчэннямі" diff --git a/features/preferences/impl/src/main/res/values-bg/translations.xml b/features/preferences/impl/src/main/res/values-bg/translations.xml index 692f0ac8f3..d5ad7facb2 100644 --- a/features/preferences/impl/src/main/res/values-bg/translations.xml +++ b/features/preferences/impl/src/main/res/values-bg/translations.xml @@ -58,6 +58,9 @@ "системни настройки" "Системните известия са изключени" "Известия" + "Тъмен" + "Светъл" + "Система" "Отстраняване на неизправности" "Отстраняване на неизправности с известията" diff --git a/features/preferences/impl/src/main/res/values-ca/translations.xml b/features/preferences/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..b2cb548d45 --- /dev/null +++ b/features/preferences/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,70 @@ + + + "Per assegurar que mai et perdis una trucada important, canvia la configuració per permetre les notificacions en pantalla completa quan el telèfon està bloquejat." + "Millora l\'experiència de les trucades" + "Tria com vols rebre les notificacions" + "Mode desenvolupador" + "Activa-ho per tenir accés a característiques i funcionalitats per a desenvolupadors." + "URL base d\'Element Call personalitzat" + "Estableix un URL base personalitzat d\'Element Call." + "URL invàlid, assegura\'t d\'incloure el protocol (http/https) i l\'adreça correctament." + "Amaga les previsualitzacions multimèdia a la cronologia" + "Puja fotos i vídeos més ràpidament i redueix l\'ús de dades" + "Optimitza qualitat de multimèdia" + "Moderació i seguretat" + "Proveïdor de notificacions push" + "Desactiva l\'editor de text enriquit per escriure Markdown manualment." + "Confirmacions de lectura" + "Si està desactivat, les confirmacions de lectura no s\'enviaran a ningú. Tot i així, rebràs les confirmacions de lectura d\'altres usuaris (que la tinguin activada)." + "Comparteix presència" + "Si està desactivat, no s\'enviaran ni rebràs confirmacions de lectura ni notificacions d\'escriptura." + "Amaga sempre" + "Mostra sempre" + "En sales privades" + "Els arxius multimèdia amagats es poden mostrar tocant-los." + "Mostra multimèdia a la cronologia" + "Activa l\'opció de mostrar el codi font dels missatges a la cronologia" + "No tens usuaris bloquejats" + "Desbloqueja" + "Podràs tornar a veure tots els seus missatges." + "Desbloqueja usuari" + "Desbloquejant…" + "Nom de visualització" + "El teu nom de visualització" + "S\'ha produït un error desconegut i no s\'ha pogut canviar la informació." + "No s\'ha pogut actualitzar el perfil" + "Edita perfil" + "Actualitzant perfil…" + "Configuració addicional" + "Trucades d\'àudio i vídeo" + "Configuració no coincident" + "S\'ha simplificat la configuració de notificacions per facilitar la cerca de les opcions. Algunes configuracions personalitzades anteriors no es mostren aquí, però encara estan actives. + +Si continues, és possible que alguna de les teves configuracions canviï." + "Xats directes" + "Configuració personalitzada per xat" + "S\'ha produït un error en actualitzar la configuració de notificacions." + "Tots els missatges" + "Mencions y paraules clau (només)" + "En xats directes, notifica\'m en" + "En xats de grup, notifica\'m en" + "Activa les notificacions en aquest dispositiu" + "La configuració no s\'ha corregit, torna-ho a intentar." + "Xats de grup" + "Invitacions" + "El servidor no admet aquesta opció en sales xifrades, és possible que no rebis notificacions en algunes sales." + "Mencions" + "Tot" + "Mencions" + "Notifica\'m a" + "Notifica\'m a @sala" + "Per rebre notificacions, canvia la teva %1$s." + "configuració del sistema" + "Notificacions del sistema desactivades" + "Notificacions" + "Fosc" + "Clar" + "Sistema" + "Soluciona problemes" + "Resolució de problemes de notificacions" + diff --git a/features/preferences/impl/src/main/res/values-cs/translations.xml b/features/preferences/impl/src/main/res/values-cs/translations.xml index 12014e7104..5c690dd824 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -87,6 +87,10 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "systémová nastavení" "Systémová oznámení byla vypnuta" "Oznámení" + "Černý" + "Tmavé" + "Světlý" + "Systém" "Historie push oznámení" "Odstraňování problémů" "Odstraňování problémů s upozorněními" diff --git a/features/preferences/impl/src/main/res/values-cy/translations.xml b/features/preferences/impl/src/main/res/values-cy/translations.xml index 9711eda179..e857f6aebe 100644 --- a/features/preferences/impl/src/main/res/values-cy/translations.xml +++ b/features/preferences/impl/src/main/res/values-cy/translations.xml @@ -70,6 +70,9 @@ Os ewch ymlaen, efallai y bydd rhai o\'ch gosodiadau\'n newid." "gosodiadau system" "Hysbysiadau system wedi\'u diffodd" "Hysbysiadau" + "Tywyll" + "Golau" + "System" "Hanes gwthio" "Datrys Problemau" "Hysbysiadau datrys problemau" diff --git a/features/preferences/impl/src/main/res/values-da/translations.xml b/features/preferences/impl/src/main/res/values-da/translations.xml index f258fb0952..459ea33632 100644 --- a/features/preferences/impl/src/main/res/values-da/translations.xml +++ b/features/preferences/impl/src/main/res/values-da/translations.xml @@ -11,6 +11,14 @@ "Skjul avatarer i anmodninger om invitation til rum" "Skjul forhåndsvisning af medier i tidslinjen" "Laboratorier" + "Den afstand, du skal tilbagelægge for at udløse en opdatering." + "Sørg for, at \"nøjagtig placering\" er aktiveret for denne app. For at ændre på tilladelsen skal du gå til %1$s." + "App-indstillinger" + "Live placeringsopdateringer" + + "Hver %1$d meter" + "Hver %1$d meter" + "Upload fotos og videoer hurtigere, og reducér dataforbrug" "Optimér mediekvaliteten" "Moderation og sikkerhed" @@ -76,6 +84,9 @@ Hvis du fortsætter, kan nogle af dine indstillinger blive ændret." "systemindstillinger" "Systemmeddelelser slået fra" "Notifikationer" + "Mørkt tema" + "Lyst tema" + "System" "Push-historik" "Fejlfind" "Fejlfinding af meddelelser" diff --git a/features/preferences/impl/src/main/res/values-de/translations.xml b/features/preferences/impl/src/main/res/values-de/translations.xml index c3722ec88c..e35cf871a1 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -76,6 +76,9 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Systemeinstellungen" "Systembenachrichtigungen deaktiviert" "Benachrichtigungen" + "Dunkel" + "Hell" + "System" "Verlauf pushen" "Fehlerbehebung" "Fehlerbehebung für Benachrichtigungen" diff --git a/features/preferences/impl/src/main/res/values-el/translations.xml b/features/preferences/impl/src/main/res/values-el/translations.xml index 64b651114d..6db52343d3 100644 --- a/features/preferences/impl/src/main/res/values-el/translations.xml +++ b/features/preferences/impl/src/main/res/values-el/translations.xml @@ -76,6 +76,9 @@ "ρυθμίσεις συστήματος" "Ειδοποιήσεις συστήματος ανενεργές" "Ειδοποιήσεις" + "Σκοτεινό" + "Φωτεινό" + "Σύστημα" "Ιστορικό push" "Αντιμετώπιση προβλημάτων" "Αντιμετώπιση προβλημάτων ειδοποιήσεων" diff --git a/features/preferences/impl/src/main/res/values-es/translations.xml b/features/preferences/impl/src/main/res/values-es/translations.xml index afdc23d90b..dd496a1273 100644 --- a/features/preferences/impl/src/main/res/values-es/translations.xml +++ b/features/preferences/impl/src/main/res/values-es/translations.xml @@ -63,6 +63,9 @@ Si continúas, es posible que algunos de tus ajustes cambien." "ajustes del sistema" "Notificaciones del sistema desactivadas" "Notificaciones" + "Oscuro" + "Claro" + "Sistema" "Historial de notificaciones push" "Solucionar problemas" "Solucionar problemas con las notificaciones" diff --git a/features/preferences/impl/src/main/res/values-et/translations.xml b/features/preferences/impl/src/main/res/values-et/translations.xml index 4e7ba1c26e..3d5102fa9f 100644 --- a/features/preferences/impl/src/main/res/values-et/translations.xml +++ b/features/preferences/impl/src/main/res/values-et/translations.xml @@ -11,6 +11,14 @@ "Peida jututubade kutsetest tunnuspildid" "Peida meedia eelvaated ajajoonel" "Katsed" + "Vahemaa, mille pead andmete uuenduse käivitamiseks läbima." + "Veendu, et sellel rakendusel on õigus kasutada funktsionaalsust „Täpne asukoht“. Õiguste muutmiseks ava %1$s." + "Rakenduse seadistused" + "Andmete uuendused reaalajas asukoha jagamisel" + + "Iga %1$d meeter" + "Iga %1$d meetrit" + "Sellega laadid fotosid ja videoid kiiremini üles ning vähendad andmemahtu" "Optimeeri meedia kvaliteeti" "Modereerimine ja ohutus" @@ -50,6 +58,7 @@ "Kas tahad katsetada?" "Katsed" "Täiendavad seadistused" + "Kõne helin" "Hääl- ja videokõned" "Eelistused ei sobi omavahel" "Et eelistusi oleks kergem leida, me oleme lihtsustanud teavituste seadistusi. Kuigi mõned varem valitud eelistused pole siin näha, siis nad kehtivad jätkuvalt. @@ -68,14 +77,34 @@ Kui sa jätkad muutmist, siis võivad muutuda ka need peidetud eelistused.""Kutsed" "Sinu koduserver ei toeta seda funktsionaalsust krüptitud jututubades ja seega ei pruugi kõik teavitused sinuni jõuda." "Mainimiste alusel" + "Vali muu ​​helin…" + "Hetkel on kasutusel %1$s" + "Sõnumi heli" + "Sõnumi heli" "Kõik" "Mainimiste alusel" "Teavita mind" "Teavita mind @jututoa puhul" + "Kohandatud" + "Sinu valitud helin…" + "Viga faili kustutamisel" + "Elementi vaikimisi väärtus" + "Elemendi hajumine" + "Viga faili importimisel" + "Viga helina eelvaate esitamisel" + "Helin" + "Sulge helinavea viga" + "Viga helina seadistamisel" + "Vaikne" + "Süsteemi vaikimisi väärtus" "Teavituste saamiseks palun muuda oma %1$s." "süsteemi seadistusi" "Süsteemi teavitused on välja lülitatud" "Teavitused" + "Süsimust kujundus" + "Tume" + "Hele" + "Süsteem" "Tõuketeadete ajalugu" "Veaotsing" "Teavituste veaotsing" diff --git a/features/preferences/impl/src/main/res/values-eu/translations.xml b/features/preferences/impl/src/main/res/values-eu/translations.xml index 42a90e0328..42416c682b 100644 --- a/features/preferences/impl/src/main/res/values-eu/translations.xml +++ b/features/preferences/impl/src/main/res/values-eu/translations.xml @@ -55,4 +55,7 @@ "sistemaren ezarpenak" "Sistemaren jakinarazpenak desaktibatuta daude" "Jakinarazpenak" + "Iluna" + "Argia" + "Sistema" diff --git a/features/preferences/impl/src/main/res/values-fa/translations.xml b/features/preferences/impl/src/main/res/values-fa/translations.xml index 05e73febb4..df42ca953c 100644 --- a/features/preferences/impl/src/main/res/values-fa/translations.xml +++ b/features/preferences/impl/src/main/res/values-fa/translations.xml @@ -19,12 +19,15 @@ "فراهم کنندهٔ آگاهی‌های ارسالی" "از کار انداختن ویرایشگر متن غنی یا نوشتن دستی مارک‌دون." "رسید‌های خواندن" + "اگر خاموش باشد، رسیدهای خوانده شدن شما برای کسی ارسال نمی‌شود. شما همچنان رسیدهای خوانده شدن را از سایر کاربران دریافت خواهید کرد." "هم‌رسانی حضور" + "اگر خاموش باشد، نمی‌توانید رسیدهای خوانده شدن یا اعلان‌های تایپ را ارسال یا دریافت کنید." "نهفتن همیشگی" "نمایش همیشگی" "در اتاق‌های خصوصی" "رسانه‌های نهفته همواره خواهند توانست با زدن رویشان نمایان شوند" "نمایش رسانه در خط زمانی" + "گزینه مشاهده منبع پیام در جدول زمانی را فعال کنید." "هیچ کاربر مسدودی ندارید" "رفع انسداد" "قادر خواهید بود دوباره همهٔ پیام‌هایش را ببینید." @@ -64,6 +67,9 @@ "تنظیمات سامانه" "آگاهی‌های سامانه‌ای خاموش شدند" "آگاهی‌ها" + "تیره" + "روشن" + "سامانه" "تاریخچهٔ آگاهی‌های ارسالی" "رفع‌اشکال" "رفع‌اشکال آگاهی‌ها" diff --git a/features/preferences/impl/src/main/res/values-fi/translations.xml b/features/preferences/impl/src/main/res/values-fi/translations.xml index f1462f3bb7..f37673427f 100644 --- a/features/preferences/impl/src/main/res/values-fi/translations.xml +++ b/features/preferences/impl/src/main/res/values-fi/translations.xml @@ -11,6 +11,14 @@ "Piilota huoneiden avatarit kutsuista" "Piilota median esikatselu aikajanalla" "Labrat" + "Etäisyys, joka sinun on kuljettava päivityksen käynnistämiseksi." + "Varmista, että tälle sovellukselle on valittu \"Tarkka sijainti\". Voit muuttaa lupaa kohdassa %1$s." + "Sovellusasetukset" + "Reaaliaikaiset sijaintipäivitykset" + + "%1$d metrin välein" + "%1$d metrin välein" + "Lähetä valokuvia ja videoita nopeammin ja vähennä datan käyttöä." "Optimoi median laatu" "Moderointi ja Turvallisuus" @@ -50,6 +58,7 @@ "Kokeilunhaluinen olo?" "Labrat" "Lisäasetukset" + "Puhelun soittoääni" "Ääni- ja videopuheluista" "Konfiguraatio ei täsmää" "Olemme yksinkertaistaneet ilmoitusasetuksia, jotta vaihtoehdot olisi helpompi löytää. Joitakin aiemmin valitsemiasi asetuksia ei näytetä tässä, mutta ne ovat edelleen voimassa. @@ -68,14 +77,34 @@ Jos jatkat, jotkin asetukset saattavat muuttua." "Kutsut" "Kotipalvelimesi ei tue tätä vaihtoehtoa salatuissa huoneissa, joten et ehkä saa ilmoitusta joissakin huoneissa." "Maininnat" + "Valitse toinen ääni…" + "Käytössä tällä hetkellä %1$s" + "Viestin ääni" + "Viestin ääni" "Kaikki" "Maininnat" "Ilmoita minulle" "Ilmoita minulle @room-maininnoista" + "Mukautettu" + "Mukautettu ääni…" + "Virhe tiedoston poistamisessa" + "Elementin oletus" + "Element Fade" + "Virhe tiedoston tuonnissa" + "Ongelma ilmoitusäänen esikatselussa" + "Ääni" + "Hylkää hälytysäänen virhe" + "Ongelma ilmoitusäänen asettamisessa" + "Hiljainen" + "Järjestelmän oletus" "Jos haluat saada ilmoituksia, vaihda %1$s." "järjestelmäsi asetuksia" "Järjestelmän ilmoitukset on poissa päältä" "Ilmoitukset" + "Musta" + "Tumma" + "Vaalea" + "Järjestelmän oletus" "Push-historia" "Vianmääritys" "Ilmoitusten vianmääritys" diff --git a/features/preferences/impl/src/main/res/values-fr/translations.xml b/features/preferences/impl/src/main/res/values-fr/translations.xml index 292b5bf9a5..fc57491f4a 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -58,6 +58,7 @@ "Envie d’expérimenter?" "Expérimental" "Réglages supplémentaires" + "Sonnerie d’appel" "Appels audio et vidéo" "Incompatibilité de configuration" "Nous avons simplifié les paramètres des notifications pour que les options soient plus faciles à trouver. Certains paramètres personnalisés que vous avez choisis par le passé ne sont pas affichés ici, mais ils sont toujours actifs. @@ -76,14 +77,34 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "Invitations" "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons." "Mentions" + "Choisir un autre son…" + "Vous utilisez actuellement %1$s" + "Son des messages" + "Son des messages" "Tous" "Mentions" "Prévenez-moi pour" "Prévenez-moi si un message contient \"@room\"" + "Personnalisé" + "Son personnalisé…" + "Erreur lors de la suppression du fichier" + "Element - défaut" + "Element - doux" + "Erreur lors de l’importation du fichier" + "Problème lors de l’écoute préalable du son d’alerte" + "Son" + "Ignorer l’erreur des sons d’alerte" + "Problème de configuration du son d’alerte" + "Silencieux" + "Paramètres par défaut du système" "Pour recevoir des notifications, veuillez modifier votre %1$s." "paramètres du système" "Les notifications du système sont désactivées" "Notifications" + "Noir" + "Sombre" + "Clair" + "Système" "Historique des Push" "Dépannage" "Dépanner les notifications" diff --git a/features/preferences/impl/src/main/res/values-hr/translations.xml b/features/preferences/impl/src/main/res/values-hr/translations.xml index 250af33d49..d101146f8c 100644 --- a/features/preferences/impl/src/main/res/values-hr/translations.xml +++ b/features/preferences/impl/src/main/res/values-hr/translations.xml @@ -11,6 +11,15 @@ "Sakrij avatare u zahtjevima za poziv u sobu" "Sakrij preglede medija na vremenskoj traci" "Laboratoriji" + "Udaljenost koju morate prijeći da biste pokrenuli ažuriranje." + "Provjerite je li \"Precizna lokacija\" omogućena za ovu aplikaciju. Za promjenu dopuštenja idite na %1$s ." + "Postavke aplikacije" + "Ažuriranja lokacije uživo" + + "Svaki %1$d metar" + "Svaki %1$d metar" + "Svaki %1$d metara" + "Brže prenesite fotografije i videozapise te smanjite potrošnju podataka" "Optimiziraj kvalitetu medija" "Moderiranje i sigurnost" @@ -76,6 +85,9 @@ Ako nastavite, neke od vaših postavki mogu se promijeniti." "postavke sustava" "Obavijesti sustava su isključene" "Obavijesti" + "Tamno" + "Svijetlo" + "Sustav" "Povijest push obavijesti" "Rješavanje problema" "Rješavanje problema s obavijestima" diff --git a/features/preferences/impl/src/main/res/values-hu/translations.xml b/features/preferences/impl/src/main/res/values-hu/translations.xml index c588e60eaf..f4a9616321 100644 --- a/features/preferences/impl/src/main/res/values-hu/translations.xml +++ b/features/preferences/impl/src/main/res/values-hu/translations.xml @@ -11,6 +11,14 @@ "Profilképek elrejtése a szobameghívókban" "Médiaelőnézetek elrejtése az idővonalon" "Kísérletek" + "A megtett távolság miután a helyadat frissül." + "Győződjön meg arról, hogy a „Pontos helymeghatározás” engedélyezve van ehhez az alkalmazáshoz. Az engedély módosításához menjen ide:%1$s ." + "Alkalmazásbeállítások" + "Élő helymeghatározás" + + "Minden %1$d méter" + "Minden %1$d méter" + "Töltse fel gyorsabban a fényképeket és videókat, valamint csökkentse az adatforgalmat" "Média minőségének optimalizálása" "Moderálás és biztonság" @@ -50,6 +58,7 @@ "Kísérletezni szeretne?" "Kísérletek" "További beállítások" + "Csengőhang" "Hang- és videóhívások" "Konfigurációs eltérés" "Egyszerűsítettük az értesítési beállításokat, hogy könnyebben megtalálhatók legyenek a lehetőségek. A korábban kiválasztott egyéni beállítások némelyike nem jelenik meg itt, de továbbra is aktív. @@ -68,14 +77,34 @@ Ha folytatja, egyes beállítások megváltozhatnak." "Meghívók" "A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést." "Említések" + "Válasszon egy másik hangot…" + "Jelenleg használt: %1$s" + "Üzenet hang" + "Üzenet hang" "Összes" "Említések" "Értesítés ezekről:" "Értesítés a @room említésekor" + "Egyedi" + "Egyedi hang…" + "Hiba a fájl törlésekor" + "Element alapértelmezett" + "Element Fade" + "Hiba a fájl importálásakor" + "Probléma a riasztási hang előnézetekor" + "Hang" + "Figyelmeztető hang hiba kikapcsolása" + "Probléma a riasztási hang beállításánál" + "Néma" + "Rendszer alapértelmezett" "Az értesítések fogadásához kérjük, módosítsa a %1$s." "rendszerbeállításokat" "A rendszerértesítések ki vannak kapcsolva" "Értesítések" + "Fekete" + "Sötét" + "Világos" + "Rendszer" "Leküldéses értesítések előzmények" "Hibaelhárítás" "Értesítések hibaelhárítása" diff --git a/features/preferences/impl/src/main/res/values-in/translations.xml b/features/preferences/impl/src/main/res/values-in/translations.xml index 5ce794c8ac..9c853aed45 100644 --- a/features/preferences/impl/src/main/res/values-in/translations.xml +++ b/features/preferences/impl/src/main/res/values-in/translations.xml @@ -72,6 +72,9 @@ Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah." "pengaturan sistem" "Pemberitahuan sistem dimatikan" "Notifikasi" + "Gelap" + "Terang" + "Sistem" "Riwayat dorongan" "Pemecahan masalah" "Pecahkan masalah notifikasi" diff --git a/features/preferences/impl/src/main/res/values-it/translations.xml b/features/preferences/impl/src/main/res/values-it/translations.xml index a10dcc9594..73555754fc 100644 --- a/features/preferences/impl/src/main/res/values-it/translations.xml +++ b/features/preferences/impl/src/main/res/values-it/translations.xml @@ -11,6 +11,14 @@ "Nascondi gli avatar nelle richieste di invito alle stanze" "Nascondi le anteprime dei media nelle conversazioni" "Labs" + "La distanza che devi percorrere per attivare un aggiornamento." + "Assicurati che l\'opzione \"Posizione precisa\" sia abilitata per questa app. Per modificare l\'autorizzazione, vai in %1$s." + "Impostazioni app" + "Aggiornamenti posizione in tempo reale" + + "Ogni %1$d metro" + "Ogni %1$d metri" + "Carica foto e video più velocemente e riduci l\'utilizzo dei dati" "Ottimizza la qualità dei contenuti multimediali" "Moderazione e Sicurezza" @@ -76,6 +84,9 @@ Se procedi, alcune delle tue impostazioni potrebbero cambiare." "impostazioni di sistema" "Notifiche di sistema disattivate" "Notifiche" + "Scuro" + "Chiaro" + "Sistema" "Cronologia push" "Risoluzione dei problemi" "Risoluzione di problemi delle notifiche" diff --git a/features/preferences/impl/src/main/res/values-ja/translations.xml b/features/preferences/impl/src/main/res/values-ja/translations.xml index 10cbca0ada..8e97d03c54 100644 --- a/features/preferences/impl/src/main/res/values-ja/translations.xml +++ b/features/preferences/impl/src/main/res/values-ja/translations.xml @@ -18,9 +18,9 @@ "%1$dm ごと" - "写真や動画を高速で送信してデータ使用量を減らす" + "写真や動画を高速で送信してデータ使用量を減らします。" "メディアの品質を最適化" - "制限と安全" + "セキュリティと制限" "自動的に画像を最適化してアップロード時間とファイルサイズを削減します。" "画像のアップロード画質を最適化" "%1$s 変更するにはタップしてください。" @@ -53,10 +53,11 @@ "プロフィールを更新中…" "スレッドへの返信を有効化" "変更を適用するためにアプリケーションは再起動します。" - "開発段階の最新機能を試します。未完成のため変更や不安定な挙動を生じる可能性があります。" - "探究したいですか?" + "開発段階の最新機能を試すことができます。未完成のため、変更や不安定な挙動が生じる可能性があります。" + "探究的な気分ですか?" "ラボ" "追加設定" + "発信音" "音声・ビデオ通話" "設定の不一致" "通知設定を簡素化し、見つけやすくしました。以前のカスタム設定の一部はここに表示されませんが、引き続き有効です。 @@ -68,21 +69,41 @@ "すべてのメッセージ" "メンションとキーワードのみ" "ダイレクトチャットで以下の通知を受け取る" - "グループチャットでは以下の通知を受け取る" + "グループチャットで以下の通知を受け取る" "この端末で通知を受け取る" "設定が修正されていません。再試行してください。" "グループチャット" "招待" "暗号化されたルームでは、この機能にホームサーバーが対応しないため、一部のルームから通知が届かない可能性があります。" "メンション" + "他の音を選択…" + "%1$s を使用中" + "メッセージの音" + "メッセージの音" "すべて" "メンション" - "以下を通知する" + "以下を通知" @ルームで通知を受け取る + "カスタム" + "カスタム着信音" + "ファイルの削除に失敗" + "Element の既定" + "Elementフェード" + "ファイルの取り込みに失敗" + "通知音のプレビューで問題が発生しました" + "音" + "通知音のエラーを無視" + "通知音の設定に問題が発生しました" + "サイレント" + "システムのデフォルト" "通知を受け取るには、%1$s を変更してください。" "システム設定" "システムで通知がオフです" "通知" + "ブラック" + "ダーク" + "ライト" + "システム" "プッシュ履歴" "トラブルシューティング" "通知のトラブルシューティング" diff --git a/features/preferences/impl/src/main/res/values-ka/translations.xml b/features/preferences/impl/src/main/res/values-ka/translations.xml index 1efe197267..5a20e718a0 100644 --- a/features/preferences/impl/src/main/res/values-ka/translations.xml +++ b/features/preferences/impl/src/main/res/values-ka/translations.xml @@ -50,6 +50,9 @@ "სისტემის პარამეტრები" "სისტემის შეტყობინებები გამორთულია" "შეტყობინებები" + "მუქი" + "ღია" + "სისტემა" "პრობლემების გადაჭრა" "პრობლემების გადაჭრის შეტყობინებები" diff --git a/features/preferences/impl/src/main/res/values-ko/translations.xml b/features/preferences/impl/src/main/res/values-ko/translations.xml index 5e17d57a87..dcff2d7b6c 100644 --- a/features/preferences/impl/src/main/res/values-ko/translations.xml +++ b/features/preferences/impl/src/main/res/values-ko/translations.xml @@ -11,6 +11,13 @@ "방 초대 요청에서 아바타 숨기기" "타임라인에서 미디어 미리 보기 숨기기" "실험실" + "위치 업데이트 전환 기준 거리" + "이 앱에 \"정확한 위치\" 권한이 허용되어 있는지 확인해 주세요. 권한을 변경하려면 %1$s(으)로 이동하세요." + "앱 설정" + "실시간 위치 업데이트" + + "%1$d미터마다" + "사진과 동영상을 더 빠르게 업로드하고 데이터 사용량을 줄이세요" "미디어 품질 최적화" "중재와 안전" @@ -51,6 +58,7 @@ "새로운 것을 시도해보고 싶으신가요?" "실험실" "추가 설정" + "통화 벨소리" "음성 및 동영상 통화" "구성 불일치" "알림 설정을 간소화하여 옵션을 더 쉽게 찾을 수 있도록 했습니다. 과거에 선택한 일부 맞춤 설정은 여기에서 표시되지 않지만, 여전히 활성화되어 있습니다. @@ -69,14 +77,34 @@ "초대" "귀하의 홈서버는 암호화된 방에서 이 옵션을 지원하지 않으므로, 일부 방에서는 알림이 표시되지 않을 수 있습니다." "언급" + "다른 소리를 선택하세요…" + "현재 사용 중: %1$s" + "메시지 소리" + "메시지 소리" "모두" "언급" "나에게 알려주세요" @room 에서 알림 받기 + "사용자 지정" + "소리 사용자 지정" + "파일 삭제 오류" + "Element 기본 설정" + "엘리먼트 페이드" + "파일 가져오기 오류" + "알림음 미리듣기 문제 발생" + "소리" + "알림음 오류 해제" + "알림 소리 설정 문제" + "무음" + "시스템 기본값" "알림을 받으려면 %1$s 을 변경해 주세요." "시스템 설정" "시스템 알림이 꺼져 있습니다." "알림" + "검은색" + "다크" + "라이트" + "시스템" "푸시 기록" "문제 해결" "문제 해결 알림" diff --git a/features/preferences/impl/src/main/res/values-nb/translations.xml b/features/preferences/impl/src/main/res/values-nb/translations.xml index 90ec12a1a1..ac67e04259 100644 --- a/features/preferences/impl/src/main/res/values-nb/translations.xml +++ b/features/preferences/impl/src/main/res/values-nb/translations.xml @@ -76,6 +76,9 @@ Hvis du fortsetter, kan noen av innstillingene dine endres." "systeminnstillinger" "Systemvarsler er slått av" "Varslinger" + "Mørk" + "Lys" + "System" "Push-historikk" "Feilsøk" "Feilsøk varsler" diff --git a/features/preferences/impl/src/main/res/values-nl/translations.xml b/features/preferences/impl/src/main/res/values-nl/translations.xml index 28614965c5..c5c439a0c3 100644 --- a/features/preferences/impl/src/main/res/values-nl/translations.xml +++ b/features/preferences/impl/src/main/res/values-nl/translations.xml @@ -54,6 +54,9 @@ Als je doorgaat, kunnen sommige van je instellingen veranderen." "systeeminstellingen" "Systeemmeldingen uitgeschakeld" "Meldingen" + "Donker" + "Licht" + "Systeem" "Problemen oplossen" "Problemen met meldingen oplossen" diff --git a/features/preferences/impl/src/main/res/values-pl/translations.xml b/features/preferences/impl/src/main/res/values-pl/translations.xml index 9e2c56e580..03be155520 100644 --- a/features/preferences/impl/src/main/res/values-pl/translations.xml +++ b/features/preferences/impl/src/main/res/values-pl/translations.xml @@ -11,6 +11,15 @@ "Ukryj awatary w prośbach o dołączenie do pokoju" "Ukryj podglądy multimediów na osi czasu" "Laboratoria" + "Odległość, jaką należy pokonać, aby uruchomić aktualizację." + "Upewnij się, że \"Dokładna lokalizacja\" jest włączona dla tej aplikacji. Aby zmienić to uprawnienie, przejdź do %1$s." + "Ustawienia aplikacji" + "Aktualizacje lokalizacji na żywo" + + "Co %1$d metr" + "Co %1$d metry" + "Co %1$d metrów" + "Przesyłaj zdjęcia i filmy szybciej, zmniejszając zużycie danych" "Optymalizuj jakość multimediów" "Moderacja i bezpieczeństwo" @@ -50,13 +59,14 @@ "Chcesz poeksperymentować?" "Laboratoria" "Dodatkowe ustawienia" + "Dzwonek połączenia" "Połączenia audio i wideo" "Niezgodność konfiguracji" "Uprościliśmy Ustawienia powiadomień, aby ułatwić nawigowanie między opcjami. Niektóre ustawienia, które wybrałeś mogły zniknąć, lecz są wciąż aktywne. Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz." "Czaty prywatne" - "Ustawienia własne wybranego czatu" + "Własne ustawienia dla wybranego czatu" "Wystąpił błąd podczas aktualizacji ustawienia powiadomień." "Wszystkie wiadomości" "Tylko wzmianki i słowa kluczowe" @@ -68,14 +78,34 @@ Niektóre ustawienia mogą ulec zmianie, jeśli kontynuujesz." "Zaproszenia" "Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z niektórych pokoi." "Wzmianki" + "Wybierz inny dźwięk…" + "Aktualnie używany %1$s" + "Dźwięk wiadomości" + "Dźwięk wiadomości" "Wszystkie" "Wzmianki" "Powiadamiaj mnie przez" "Powiadom mnie na @pokój" + "Własny" + "Własny dźwięk…" + "Błąd usuwania pliku" + "Domyślny Element" + "Element Fade" + "Błąd importowania pliku" + "Wystąpił błąd podczas podglądania dźwięku powiadomień" + "Dźwięk" + "Pomiń dźwięk błędu" + "Nie udało się ustawić dźwięku powiadomień" + "Cichy" + "Ustawienie domyślne" "Aby otrzymywać powiadomienia, zmień swoje%1$s." "ustawienia systemowe" "Powiadomienia systemowe wyłączone" "Powiadomienia" + "Czarny" + "Ciemny" + "Jasny" + "System" "Historia powiadomień Push" "Rozwiązywanie problemów" "Rozwiązywanie problemów powiadomień" diff --git a/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml b/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml index f6e1bc90ba..be354681f5 100644 --- a/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml @@ -76,6 +76,9 @@ Se você continuar, algumas de suas configurações poderão mudar." "configurações do seu sistema" "Notificações do sistema desativadas" "Notificações" + "Escuro" + "Claro" + "Sistema" "Histórico de push" "Solução de problemas" "Solucionar problemas de notificações" diff --git a/features/preferences/impl/src/main/res/values-pt/translations.xml b/features/preferences/impl/src/main/res/values-pt/translations.xml index ffd38b961f..0e27d58de5 100644 --- a/features/preferences/impl/src/main/res/values-pt/translations.xml +++ b/features/preferences/impl/src/main/res/values-pt/translations.xml @@ -76,6 +76,9 @@ Se prosseguires, algumas delas podem ser alteradas." "configurações do sistema" "Notificações do sistema desativadas" "Notificações" + "Escuro" + "Claro" + "Sistema" "Histórico de push" "Resolução de problemas" "Corrigir notificações" diff --git a/features/preferences/impl/src/main/res/values-ro/translations.xml b/features/preferences/impl/src/main/res/values-ro/translations.xml index 6f8e41c5b9..69eddf05f5 100644 --- a/features/preferences/impl/src/main/res/values-ro/translations.xml +++ b/features/preferences/impl/src/main/res/values-ro/translations.xml @@ -11,6 +11,15 @@ "Ascundeți avatarele din invitațiile pentru camere" "Ascundeți previzualizările media în lista de mesaje" "Laboratoare" + "Distanța pe care trebuie să o parcurgeți pentru a declanșa o actualizare." + "Asigurați-vă că este activată opțiunea „Locație precisă” pentru această aplicație. Pentru a schimba permisiunea, accesați %1$s." + "Setările aplicației" + "Actualizări in timp real ale locației" + + "La fiecare %1$d metru" + "La fiecare %1$d metri" + "La fiecare %1$d metri" + "Încărcați fotografii și videoclipuri mai rapid și reduceți consumul de date" "Optimizați calitatea media" "Moderare și siguranță" @@ -78,6 +87,10 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate.""Setări de sistem" "Notificările de sistem sunt dezactivate" "Notificări" + "Negru" + "Întunecat" + "Deschis" + "Sistem" "Istoricul notificărilor" "Depanare" "Depanați notificările" 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 77be6fd8db..e81bd50021 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -11,11 +11,20 @@ "Скрывать аватары в приглашениях" "Скрывать предпросмотр медиа в истории сообщений" "Лаборатория" + "Расстояние, которое нужно пройти, чтобы инициировать обновление." + "Убедитесь, что для этого приложения включена функция «Точное местоположение». Чтобы изменить разрешение, перейдите на страницу %1$s." + "Настройки приложения" + "Обновления местоположения в режиме реального времени" + + "Каждый %1$d метр" + "Каждые %1$d метра" + "Каждые %1$d метров" + "Загружайте фотографии и видео быстрее и сокращайте потребление трафика" "Оптимизировать качество мультимедиа" "Модерация и безопасность" - "Автоматически оптимизируйте изображения для более быстрой загрузки и уменьшения размера файлов." - "Оптимизируйте качество загрузки изображения" + "Автоматически оптимизировать изображения для более быстрой загрузки и уменьшения размера файлов." + "Оптимизировать качество загрузки изображений" "%1$s. Нажмите здесь, чтобы изменить." "Высокое (1080p)" "Низкое (480p)" @@ -26,14 +35,14 @@ "Уведомления о прочтении" "Если этот параметр выключен, другие пользователи не будут видеть, прочитали ли вы сообщения. Вы по-прежнему будете видеть статус прочтения других пользователей." "Делиться присутствием" - "Если выключено, вы не будете видеть, кто печатает и читает сообщения, а также другие пользователи не будут знать, когда вы печатаете или читаете сообщения." + "Если выключено, Вы не будете видеть, кто печатает и читает сообщения, а также другие пользователи не будут знать, когда Вы печатаете или читаете сообщения." "Всегда скрывать" "Всегда показывать" "В приватных комнатах" "Скрытые медиа всегда можно просмотреть, нажав на них." "Показать медиа в истории сообщений" "Включить опцию просмотра источника сообщения в истории сообщений." - "У вас нет заблокированных пользователей" + "У Вас нет заблокированных пользователей" "Разблокировать" "Вы снова сможете увидеть все сообщения." "Разблокировать пользователя" @@ -50,11 +59,12 @@ "Хотите попробовать?" "Лаборатория" "Дополнительные параметры" + "Мелодия звонка" "Аудио и видео звонки" "Несоответствие конфигурации" - "Мы упростили настройки уведомлений, чтобы упростить поиск опций. Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. + "Мы упростили настройки уведомлений, чтобы упростить поиск опций. Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они всё ещё активны. -Если вы продолжите, некоторые настройки могут быть изменены." +Если Вы продолжите, некоторые настройки могут быть изменены." "В личных чатах" "Отдельные настройки для каждого чата" "Произошла ошибка при обновлении настройки уведомления." @@ -63,20 +73,40 @@ "Уведомлять меня в личных чатах" "Уведомлять меня в групповых чатах" "Включить уведомления на данном устройстве" - "Конфигурация не была исправлена, попробуйте еще раз." + "Конфигурация не была исправлена, попробуйте ещё раз." "В групповых чатах" "Приглашения" - "Ваш сервер не поддерживает эту опцию для зашифрованных комнат, вы можете не получать некоторые уведомления." + "Ваш сервер не поддерживает эту опцию для зашифрованных комнат, Вы можете не получать некоторые уведомления." "Упоминания" + "Выбрать другой звук…" + "Сейчас используется %1$s" + "Звук сообщения" + "Звук сообщения" "Все" "Упоминания" "Уведомлять меня" "Уведомлять меня при упоминании @room" + "Пользовательский" + "Пользовательский звук…" + "Ошибка при удалении файла" + "Element по умолчанию" + "Element Fade" + "Ошибка импорта файла" + "Ошибка предварительного прослушиванием звука" + "Звук" + "Отклонить ошибку о звуке" + "Проблема с настройкой звукового оповещения" + "Без звука" + "Как в системе" "Чтобы получать уведомления, измените %1$s." "системные настройки" "Системные уведомления выключены" "Уведомления" + "Черный" + "Темная" + "Светлое" + "Системное" "История уведомлений" "Устранение неполадок" - "Уведомления об устранении неполадок" + "Устранение неполадок с уведомлениями" diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml index 9968c6d4cb..530fccd90b 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -78,6 +78,9 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""nastavenia systému" "Systémové oznámenia sú vypnuté" "Oznámenia" + "Tmavý" + "Svetlý" + "Systém" "História push oznámení" "Riešenie problémov" "Oznámenia riešení problémov" diff --git a/features/preferences/impl/src/main/res/values-sv/translations.xml b/features/preferences/impl/src/main/res/values-sv/translations.xml index e25e36a44f..109000440f 100644 --- a/features/preferences/impl/src/main/res/values-sv/translations.xml +++ b/features/preferences/impl/src/main/res/values-sv/translations.xml @@ -70,6 +70,9 @@ Om du fortsätter kan vissa av dina inställningar ändras." "systeminställningar" "Systemaviseringar avstängda" "Aviseringar" + "Mörkt" + "Ljust" + "System" "Push-historik" "Felsök" "Felsök aviseringar" diff --git a/features/preferences/impl/src/main/res/values-tr/translations.xml b/features/preferences/impl/src/main/res/values-tr/translations.xml index 0df8500fda..78e9018554 100644 --- a/features/preferences/impl/src/main/res/values-tr/translations.xml +++ b/features/preferences/impl/src/main/res/values-tr/translations.xml @@ -74,6 +74,9 @@ Devam ederseniz, bazı ayarlarınız değişebilir." "si̇stem ayarları" "Sistem bildirimleri kapalı" "Bildirimler" + "Koyu" + "Aydınlık" + "Sistem" "Sorun gider" "Sorun Giderme Bildirimleri" diff --git a/features/preferences/impl/src/main/res/values-uk/translations.xml b/features/preferences/impl/src/main/res/values-uk/translations.xml index d145724af8..11b6fae53e 100644 --- a/features/preferences/impl/src/main/res/values-uk/translations.xml +++ b/features/preferences/impl/src/main/res/values-uk/translations.xml @@ -11,6 +11,15 @@ "Сховати аватари у запитах на запрошення до кімнат" "Сховати попередній перегляд медіа у стрічці" "Лабораторії" + "Відстань, яку потрібно пройти, щоб ініціювати оновлення." + "Переконайтеся, що для цього додатка увімкнено функцію «Точна геолокація». Щоб змінити дозвіл, перейдіть на сторінку %1$s." + "Налаштування додатка" + "Оновлення місцезнаходження в реальному часі" + + "Кожен %1$d метр" + "Кожні %1$d метри" + "Кожні %1$d метрів" + "Швидше завантажуйте фотографії та відео та зменшуйте використання даних" "Оптимізуйте медіаякість" "Модерування й безпека" @@ -72,10 +81,22 @@ "Згадки" "Сповіщати мене про" "Сповіщати про @room" + "Користувацький звук…" + "Помилка видалення файлу" + "За замовчуванням Element" + "Згасання Element" + "Помилка імпорту файлу" + "Проблема з попереднім прослуховуванням звуку сповіщення" + "Звук" + "Проблема налаштовування звуку сповіщення" "Щоб отримувати сповіщення змініть свої %1$s." "системні налаштування" "Системні сповіщення вимкнені" "Сповіщення" + "Темна" + "Темна" + "Світла" + "Системна" "Історія push-сповіщень" "Усунення несправностей" "Усунення неполадок сповіщень" diff --git a/features/preferences/impl/src/main/res/values-ur/translations.xml b/features/preferences/impl/src/main/res/values-ur/translations.xml index 470763e877..35b8f4aa4e 100644 --- a/features/preferences/impl/src/main/res/values-ur/translations.xml +++ b/features/preferences/impl/src/main/res/values-ur/translations.xml @@ -53,6 +53,9 @@ "نظام کی ترتیبات" "نظام کی اطلاعات بند کر دی گئیں" "اطلاعات" + "اندھیرا" + "روشنی" + "نظام" "ازالہ کریں" "اطلاعات کا ازالہ کریں" diff --git a/features/preferences/impl/src/main/res/values-uz/translations.xml b/features/preferences/impl/src/main/res/values-uz/translations.xml index 9c81fc3daa..884027f9b8 100644 --- a/features/preferences/impl/src/main/res/values-uz/translations.xml +++ b/features/preferences/impl/src/main/res/values-uz/translations.xml @@ -76,6 +76,9 @@ Davom ettirsangiz, baʼzi sozlamalaringiz oʻzgarishi mumkin." "tizim sozlamalari" "Tizim bildirishnomalari o\'chirilgan" "Bildirishnomalar" + "Tungi" + "Nur" + "Tizim" "Bildirishnoma tarixi" "Muammolarni bartaraf etish" "Bildirishnomalar bilan bog‘liq muammolarni bartaraf etish" diff --git a/features/preferences/impl/src/main/res/values-vi/translations.xml b/features/preferences/impl/src/main/res/values-vi/translations.xml index 9f24244b7e..9889b1697a 100644 --- a/features/preferences/impl/src/main/res/values-vi/translations.xml +++ b/features/preferences/impl/src/main/res/values-vi/translations.xml @@ -61,6 +61,9 @@ Nếu bạn tiếp tục, một số cài đặt của bạn có thể thay đ "cài đặt hệ thống" "Thông báo hệ thống đã tắt" "Thông báo" + "Tối" + "Sáng" + "Hệ thống" "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-rTW/translations.xml b/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml index 634911100a..03892ce980 100644 --- a/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh-rTW/translations.xml @@ -76,6 +76,9 @@ "系統設定" "已關閉系統通知" "通知" + "深色" + "淺色" + "系統" "推播通知歷史紀錄" "疑難排解" "疑難排解通知" 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 d57fb3b740..0b8bbd2a07 100644 --- a/features/preferences/impl/src/main/res/values-zh/translations.xml +++ b/features/preferences/impl/src/main/res/values-zh/translations.xml @@ -12,7 +12,7 @@ "在时间线上隐藏媒体预览" "实验室" "触发一次更新所需的移动距离。" - "确保已为 app 启用“精确位置”。如需更改该权限请转到 %1$s 。" + "确保已为 app 启用“精确位置”。如需更改该权限请转到 %1$s。" "App 设置" "实时位置更新" @@ -44,19 +44,20 @@ "解除屏蔽" "可以重新接收他们的消息。" "解除屏蔽用户" - "正在解除屏蔽……" + "正在解除屏蔽…" "显示名称" "你的显示名称" "遇到未知错误,无法更改信息。" "无法更新个人资料" "编辑个人资料" - "更新个人资料……" + "正在更新个人资料…" "启用消息列中的回复" "App 将重启以应用此更改。" "尝试我们最新的开发理念。这些功能尚未最终确定,可能不稳定,也可能会发生变化。" "想尝试新功能?" "实验室" "更多设置" + "来电铃声" "音视频通话" "配置不匹配" "我们简化了通知设置,使选项更易于查找。你曾经选择的某些自定义设置未在此处显示,但它们仍然有效。 @@ -75,14 +76,34 @@ "邀请" "主服务器不支持在加密房间中的此选项,因此在某些房间你可能无法收到通知。" "提及" + "选择其它提示音…" + "当前正在使用 %1$s" + "消息提示音" + "消息提示音" "全部" "提及" "通知我以下类型" - "我在房间中被提及时通知我" + "提及所有成员(@room)时通知我" + "自定义" + "自定义提示音…" + "删除文件时出错" + "Element 默认" + "Element 淡入" + "导入文件时出错" + "预览提示音时出现问题" + "提示音" + "忽略提示音错误" + "设置提示音时出现问题" + "静默" + "系统默认" "要接收通知,请更改 %1$s。" "系统设置" "系统通知已关闭" "通知" + "纯黑" + "深色" + "浅色" + "系统" "推送历史" "排查问题" "排查通知问题" diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index d5abe1df6a..cca92d0032 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -58,6 +58,7 @@ "Feeling experimental?" "Labs" "Additional settings" + "Call ringtone" "Audio and video calls" "Configuration mismatch" "We’ve simplified Notifications Settings to make options easier to find. Some custom settings you’ve chosen in the past are not shown here, but they’re still active. @@ -76,14 +77,34 @@ If you proceed, some of your settings may change." "Invitations" "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms." "Mentions" + "Choose another sound…" + "Currently using %1$s" + "Message sound" + "Message sound" "All" "Mentions" "Notify me for" "Notify me on @room" + "Custom" + "Custom sound…" + "Error deleting file" + "Element Default" + "Element Fade" + "Error importing file" + "Problem previewing alert sound" + "Sound" + "Dismiss alert sound error" + "Problem setting alert sound" + "Silent" + "System Default" "To receive notifications, please change your %1$s." "system settings" "System notifications turned off" "Notifications" + "Black" + "Dark" + "Light" + "System" "Push history" "Troubleshoot" "Troubleshoot notifications" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt index 258e9855de..6d18bfeebf 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.about import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -19,51 +21,46 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class AboutViewTest { - @get:Rule val rule = createAndroidComposeRule() +class AboutViewTest : RobolectricTest() { @Test - fun `clicking on back invokes back callback`() { + fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setAboutView( + setAboutView( anAboutState(), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on an item invokes the expected callback`() { + fun `clicking on an item invokes the expected callback`() = runAndroidComposeUiTest { val state = anAboutState() ensureCalledOnceWithParam(state.elementLegals.first()) { callback -> - rule.setAboutView( + setAboutView( state, onElementLegalClick = callback, ) - rule.clickOn(state.elementLegals.first().titleRes) + clickOn(state.elementLegals.first().titleRes) } } @Test - fun `clicking on the open source licenses invokes the expected callback`() { + fun `clicking on the open source licenses invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setAboutView( + setAboutView( anAboutState(), onOpenSourceLicensesClick = callback, ) - rule.clickOn(CommonStrings.common_open_source_licenses) + clickOn(CommonStrings.common_open_source_licenses) } } } -private fun AndroidComposeTestRule.setAboutView( +private fun AndroidComposeUiTest.setAboutView( state: AboutState, onElementLegalClick: (ElementLegal) -> Unit = EnsureNeverCalledWithParam(), onOpenSourceLicensesClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index fe121e4fe9..25d7c01778 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.media.MediaPreviewValue +import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore @@ -209,6 +210,46 @@ class AdvancedSettingsPresenterTest { } } + @Test + fun `present - exposes live location minimum distance from app preferences`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore( + liveLocationMinimumDistanceUpdate = 50, + ) + val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + + with(awaitItem()) { + assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(50) + } + } + } + + @Test + fun `present - saving live location minimum distance updates app preferences`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore( + liveLocationMinimumDistanceUpdate = 10, + ) + val presenter = createAdvancedSettingsPresenter(appPreferencesStore = appPreferencesStore) + + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + + with(awaitItem()) { + assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(10) + eventSink(AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate(42)) + } + with(awaitItem()) { + assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(42) + } + } + } + @Test fun `present - black theme option shown when feature flag enabled`() = runTest { val presenter = createAdvancedSettingsPresenter( @@ -338,7 +379,7 @@ class AdvancedSettingsPresenterTest { } private fun CoroutineScope.createAdvancedSettingsPresenter( - appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), mediaPreviewConfigStateStore: MediaPreviewConfigStateStore = FakeMediaPreviewConfigStateStore(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt index e46e350415..fa4ecfd829 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.advanced import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.preferences.impl.R @@ -29,105 +31,99 @@ 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.pressBack +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.toImmutableList -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 AdvancedSettingsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class AdvancedSettingsViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on other theme emits the expected event`() { + fun `clicking on other theme emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.common_appearance) - rule.clickOn(CommonStrings.common_dark) + clickOn(CommonStrings.common_appearance) + clickOn(R.string.theme_dark) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) } @Test - fun `black theme is shown when available`() { - rule.setAdvancedSettingsView( + fun `black theme is shown when available`() = runAndroidComposeUiTest { + setAdvancedSettingsView( state = aAdvancedSettingsState( availableThemeOptions = ThemeOption.entries.toImmutableList(), ), ) - rule.clickOn(CommonStrings.common_appearance) - rule.run { - val text = activity.getString(CommonStrings.common_black) + clickOn(CommonStrings.common_appearance) + run { + val text = activity!!.getString(R.string.theme_black) onNodeWithText(text).assertExists() } } @Test - fun `black theme is hidden when unavailable`() { - rule.setAdvancedSettingsView( + fun `black theme is hidden when unavailable`() = runAndroidComposeUiTest { + setAdvancedSettingsView( state = aAdvancedSettingsState( availableThemeOptions = ThemeOption.entries.filterNot { it == ThemeOption.Black }.toImmutableList(), ), ) - rule.clickOn(CommonStrings.common_appearance) - rule.assertNoNodeWithText(CommonStrings.common_black) + clickOn(CommonStrings.common_appearance) + assertNoNodeWithText(R.string.theme_black) } @Test - fun `clicking on View source emits the expected event`() { + fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_view_source) + clickOn(CommonStrings.action_view_source) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) } @Test - fun `clicking on Share presence emits the expected event`() { + fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_advanced_settings_share_presence) + clickOn(R.string.screen_advanced_settings_share_presence) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) } @Test - fun `clicking on media to enable compression emits the expected event`() { + fun `clicking on media to enable compression emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), analyticsService = analyticsService ) - rule.clickOn(R.string.screen_advanced_settings_media_compression_description) + clickOn(R.string.screen_advanced_settings_media_compression_description) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( @@ -139,17 +135,17 @@ class AdvancedSettingsViewTest { } @Test - fun `clicking on media to disable compression emits the expected event`() { + fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( mediaOptimizationState = MediaOptimizationState.AllMedia(isEnabled = true), eventSink = eventsRecorder, ), analyticsService = analyticsService ) - rule.clickOn(R.string.screen_advanced_settings_media_compression_description) + clickOn(R.string.screen_advanced_settings_media_compression_description) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( @@ -162,65 +158,65 @@ class AdvancedSettingsViewTest { @Test @Config(qualifiers = "h1080dp") - fun `clicking on hide invite avatars emits the expected event`() { + fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, hideInviteAvatars = false ), ) - rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetHideInviteAvatars(true)) } @Test @Config(qualifiers = "h1080dp") - fun `clicking on timeline media preview always hide emits the expected event`() { + fun `clicking on timeline media preview always hide emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On ), ) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) } @Test @Config(qualifiers = "h1080dp") - fun `clicking on timeline media preview private rooms emits the expected event`() { + fun `clicking on timeline media preview private rooms emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On ), ) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) } @Test @Config(qualifiers = "h1080dp") - fun `clicking on timeline media preview always show emits the expected event`() { + fun `clicking on timeline media preview always show emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.Off ), ) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) + clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) } @Test @Config(qualifiers = "h1080dp") - fun `hide invite avatars toggle is disabled when action is loading`() { + fun `hide invite avatars toggle is disabled when action is loading`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, hideInviteAvatars = false, @@ -228,14 +224,14 @@ class AdvancedSettingsViewTest { ), ) // The toggle should be disabled, so clicking should not emit any events - rule.clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) + clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) } @Test @Config(qualifiers = "h1080dp") - fun `timeline media preview options are disabled when action is loading`() { + fun `timeline media preview options are disabled when action is loading`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setAdvancedSettingsView( + setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, timelineMediaPreviewValue = MediaPreviewValue.On, @@ -243,15 +239,16 @@ class AdvancedSettingsViewTest { ), ) // The options should be disabled, so clicking should not emit any events - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) - rule.clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) + clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) + clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) } } -private fun AndroidComposeTestRule.setAdvancedSettingsView( +private fun AndroidComposeUiTest.setAdvancedSettingsView( state: AdvancedSettingsState, analyticsService: AnalyticsService = FakeAnalyticsService(), onBackClick: () -> Unit = EnsureNeverCalled(), + onOpenAppSettings: () -> Unit = EnsureNeverCalled(), ) { setContent { CompositionLocalProvider( @@ -260,6 +257,7 @@ private fun AndroidComposeTestRule.setAdvan AdvancedSettingsView( state = state, onBackClick = onBackClick, + onOpenAppSettingsClick = onOpenAppSettings ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt index b3549762ab..467ace4675 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.blockedusers import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.components.aMatrixUserList @@ -23,72 +25,66 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class BlockedUserViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class BlockedUserViewTest : RobolectricTest() { @Test - fun `clicking on back invokes back callback`() { + fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on a user emits the expected Event`() { + fun `clicking on a user emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val userList = aMatrixUserList() - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( blockedUsers = userList, eventSink = eventsRecorder ), ) - rule.onNodeWithText(userList.first().displayName.orEmpty()).performClick() + onNodeWithText(userList.first().displayName.orEmpty()).performClick() eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId)) } @Test - fun `clicking on cancel sends a BlockedUsersEvents`() { + fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(BlockedUsersEvents.Cancel) } @Test - fun `clicking on confirm sends a BlockedUsersEvents`() { + fun `clicking on confirm sends a BlockedUsersEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setBlockedUsersView( + setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_blocked_users_unblock_alert_action) + clickOn(R.string.screen_blocked_users_unblock_alert_action) eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock) } } -private fun AndroidComposeTestRule.setBlockedUsersView( +private fun AndroidComposeUiTest.setBlockedUsersView( state: BlockedUsersState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { 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 d4d02d7de9..cc9d3cdf8f 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 @@ -6,90 +6,86 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.developer import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.preferences.impl.R 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class DeveloperSettingsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class DeveloperSettingsViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h2000dp") @Test - fun `clicking on push history notification invokes the expected callback`() { + fun `clicking on push history notification invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onPushHistoryClick = it ) - rule.clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) + clickOn(R.string.troubleshoot_notifications_entry_point_push_history_title) } } @Config(qualifiers = "h2000dp") @Test - fun `clicking on open showkase invokes the expected callback`() { + fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), onOpenShowkase = it ) - rule.onNodeWithText("Open Showkase browser").performClick() + onNodeWithText("Open Showkase browser").performClick() } } @Config(qualifiers = "h2200dp") @Test - fun `clicking on clear cache emits the expected event`() { + fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setDeveloperSettingsView( + setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), ) - rule.onNodeWithText("Clear cache").performClick() + onNodeWithText("Clear cache").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) } } -private fun AndroidComposeTestRule.setDeveloperSettingsView( +private fun AndroidComposeUiTest.setDeveloperSettingsView( state: DeveloperSettingsState, onOpenShowkase: () -> Unit = EnsureNeverCalled(), onPushHistoryClick: () -> Unit = EnsureNeverCalled(), 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 index 123f31ae8e..3500b96731 100644 --- 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 @@ -5,20 +5,22 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.developer.appsettings import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi 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 androidx.compose.ui.test.v2.runAndroidComposeUiTest 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 @@ -27,78 +29,72 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest 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() - +class AppDeveloperSettingsPageTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Config(qualifiers = "h1500dp") @Test - fun `clicking on element call url open the dialogs and submit emits the expected event`() { + fun `clicking on element call url open the dialogs and submit emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAppDeveloperSettingsView( + 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())) + clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) textInputNode.performTextInput("https://call.element.dev") - rule.clickOn(CommonStrings.action_ok) + 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`() { + fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), onOpenShowkase = it ) - rule.onNodeWithText("Open Showkase browser").performClick() + onNodeWithText("Open Showkase browser").performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on log level emits the expected event`() { + fun `clicking on log level emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAppDeveloperSettingsView( + setAppDeveloperSettingsView( state = anAppDeveloperSettingsState( eventSink = eventsRecorder ), ) - rule.onNodeWithText("Tracing log level").performClick() - rule.onNodeWithText("Debug").performClick() + onNodeWithText("Tracing log level").performClick() + onNodeWithText("Debug").performClick() eventsRecorder.assertSingle(AppDeveloperSettingsEvent.SetTracingLogLevel(LogLevelItem.DEBUG)) } } -private fun AndroidComposeTestRule.setAppDeveloperSettingsView( +private fun AndroidComposeUiTest.setAppDeveloperSettingsView( state: AppDeveloperSettingsState, onOpenShowkase: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), 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 index 0e9d774e84..3d2cc85e54 100644 --- 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 @@ -13,13 +13,9 @@ 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 @@ -68,18 +64,6 @@ class AppDeveloperSettingsPresenterTest { } } - @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() @@ -156,13 +140,11 @@ class AppDeveloperSettingsPresenterTest { } ), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), - buildMeta: BuildMeta = aBuildMeta(), ): AppDeveloperSettingsPresenter { return AppDeveloperSettingsPresenter( featureFlagService = featureFlagService, rageshakePresenter = { aRageshakePreferencesState() }, appPreferencesStore = preferencesStore, - buildMeta = buildMeta, ) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt index e03b65f0a9..509d00db25 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt @@ -198,7 +198,7 @@ class EditDefaultNotificationSettingsPresenterTest { ): EditDefaultNotificationSettingPresenter { return EditDefaultNotificationSettingPresenter( notificationSettingsService = notificationSettingsService, - isOneToOne = false, + isDm = false, roomListService = roomListService, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt index 9b36c477a4..29e4b1c40a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt @@ -17,21 +17,35 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.api.notifications.NotificationSoundUpdater +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier import io.element.android.libraries.push.test.FakePushService +import io.element.android.libraries.push.test.notifications.FakeNotificationSoundUpdater +import io.element.android.libraries.push.test.notifications.FakeSoundDisplayNameResolver +import io.element.android.libraries.push.test.notifications.sound.FakeNotificationSoundCopier import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.test.FakePushProvider import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.consumeItemsUntilPredicate 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.flow.first import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.time.Duration.Companion.milliseconds +@Suppress("LargeClass") class NotificationSettingsPresenterTest { @Test fun `present - ensures initial state is correct`() = runTest { @@ -61,8 +75,8 @@ class NotificationSettingsPresenterTest { val notificationSettingsService = FakeNotificationSettingsService() val presenter = createNotificationSettingsPresenter(notificationSettingsService) presenter.test { - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isDM = false, mode = RoomNotificationMode.ALL_MESSAGES) + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isDM = false, mode = RoomNotificationMode.ALL_MESSAGES) val updatedState = consumeItemsUntilPredicate { (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid) ?.defaultGroupNotificationMode == RoomNotificationMode.ALL_MESSAGES @@ -79,12 +93,12 @@ class NotificationSettingsPresenterTest { presenter.test { notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = true, - isOneToOne = false, + isDM = false, mode = RoomNotificationMode.ALL_MESSAGES ) notificationSettingsService.setDefaultRoomNotificationMode( isEncrypted = false, - isOneToOne = false, + isDM = false, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY ) val updatedState = consumeItemsUntilPredicate { @@ -353,10 +367,756 @@ class NotificationSettingsPresenterTest { ) } + @Test + fun `present - SetMessageSound atomically persists, increments version, and recreates channel`() = runTest { + val recreateRecorder = lambdaRecorder { _, _ -> } + val appPreferencesStore = InMemoryAppPreferencesStore() + val customSound = NotificationSound.Custom("content://media/internal/audio/media/42") + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { sound, version -> recreateRecorder(sound, version) }, + ), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(initialState.messageSound.sound).isEqualTo(NotificationSound.SystemDefault) + + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(customSound)) + + val updatedState = consumeItemsUntilPredicate { it.messageSound.sound == customSound }.last() + assertThat(updatedState.messageSound.sound).isEqualTo(customSound) + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSoundVersion).isEqualTo(1) + recreateRecorder.assertions().isCalledOnce().with(value(customSound), value(1)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SetCallRingtone with Silent atomically persists and recreates channel`() = runTest { + val recreateRecorder = lambdaRecorder { _, _ -> } + val appPreferencesStore = InMemoryAppPreferencesStore() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateRingingCallChannelLambda = { sound, version -> recreateRecorder(sound, version) }, + ), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(initialState.callRingtone.sound).isEqualTo(NotificationSound.SystemDefault) + + initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + + val updatedState = consumeItemsUntilPredicate { it.callRingtone.sound == NotificationSound.Silent }.last() + assertThat(updatedState.callRingtone.sound).isEqualTo(NotificationSound.Silent) + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().callRingtoneVersion).isEqualTo(1) + recreateRecorder.assertions().isCalledOnce().with(value(NotificationSound.Silent), value(1)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - successive message sound changes increment version each time`() = runTest { + val versions = mutableListOf() + val appPreferencesStore = InMemoryAppPreferencesStore() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { _, version -> versions += version }, + ), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://a"))) + consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.Custom("content://a") } + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://b"))) + consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.Custom("content://b") } + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) + consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.SystemDefault } + + assertThat(versions).containsExactly(1, 2, 3).inOrder() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SetMessageSound with SystemDefault clears existing custom URI`() = runTest { + val recreateRecorder = lambdaRecorder { _, _ -> } + val appPreferencesStore = InMemoryAppPreferencesStore(messageSound = NotificationSound.Custom("content://existing")) + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { sound, version -> recreateRecorder(sound, version) }, + ), + ) + presenter.test { + consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.messageSound.sound == NotificationSound.Custom("content://existing") + }.last().eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) + consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.SystemDefault } + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(NotificationSound.SystemDefault) + recreateRecorder.assertions().isCalledOnce().with(value(NotificationSound.SystemDefault), value(1)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SetMessageSound surfaces DataStore failure via changeNotificationSettingAction`() = runTest { + val backing = InMemoryAppPreferencesStore() + val store = object : AppPreferencesStore by backing { + override suspend fun setMessageSoundAndIncrementVersion(sound: NotificationSound, title: String?): Int { + error("Simulated DataStore failure") + } + } + val presenter = createNotificationSettingsPresenter(appPreferencesStore = store) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://x"))) + + val errorState = consumeItemsUntilPredicate { + it.changeNotificationSettingAction.isFailure() + }.last() + assertThat(errorState.changeNotificationSettingAction.isFailure()).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SystemDefault and Silent display names do not invoke the resolver`() = runTest { + val resolverCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.SystemDefault, + callRingtone = NotificationSound.Silent, + ), + soundDisplayNameResolver = FakeSoundDisplayNameResolver( + resolveLambda = { uri -> + resolverCalls += uri + "Should not appear" + }, + ), + ) + presenter.test { + // The synchronous branches (SystemDefault / Silent) feed the label directly without + // routing through produceState — so the resolver is never invoked, and the label + // never flashes from the default to itself. + consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(resolverCalls).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ElementDefault and ElementFade display names do not invoke the resolver`() = runTest { + val resolverCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.ElementFade, + callRingtone = NotificationSound.ElementDefault, + ), + soundDisplayNameResolver = FakeSoundDisplayNameResolver( + resolveLambda = { uri -> + resolverCalls += uri + "Should not appear" + }, + ), + ) + presenter.test { + consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(resolverCalls).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - resolves Custom display name via SoundDisplayNameResolver`() = runTest { + val resolverCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(messageSound = NotificationSound.Custom("content://media/42")), + soundDisplayNameResolver = FakeSoundDisplayNameResolver( + resolveLambda = { uri -> + resolverCalls += uri + "Pixel notification" + }, + ), + ) + presenter.test { + val state = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.messageSound.displayName == "Pixel notification" + }.last() + assertThat(state.messageSound.displayName).isEqualTo("Pixel notification") + assertThat(resolverCalls).contains("content://media/42") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - Custom display name falls back to localized Custom string when no persisted title and resolver returns null`() = runTest { + val resolverCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + // No persisted display name (legacy data path). Resolver returns null. Final fallback + // is the localised string resource (FakeStringProvider returns "A string" for any + // resource id) — never the default label, so the row is not mislabelled as "System default". + appPreferencesStore = InMemoryAppPreferencesStore(messageSound = NotificationSound.Custom("content://gone")), + soundDisplayNameResolver = FakeSoundDisplayNameResolver( + resolveLambda = { uri -> + resolverCalls += uri + null + }, + ), + ) + presenter.test { + val state = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.messageSound.sound is NotificationSound.Custom && + it.messageSound.displayName == "A string" + }.last() + assertThat(state.messageSound.displayName).isEqualTo("A string") + assertThat(resolverCalls).contains("content://gone") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - persisted in-app pick takes precedence over channel read`() = runTest { + // When DataStore holds a real in-app pick, the channel read must not run. Persisted + // value is what the user explicitly chose; channel state is only a fallback. + var channelReadCalls = 0 + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.Custom("content://persisted"), + messageSoundDisplayName = "Persisted Tone", + ), + notificationSoundUpdater = FakeNotificationSoundUpdater( + readNoisyChannelSoundLambda = { + channelReadCalls++ + NotificationSound.Custom("content://channel/should-not-appear") + }, + ), + ) + presenter.test { + val state = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.messageSound.displayName == "Persisted Tone" + }.last() + assertThat(state.messageSound.sound).isEqualTo(NotificationSound.Custom("content://persisted")) + assertThat(channelReadCalls).isEqualTo(0) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - persisted display name takes precedence over live resolver probe`() = runTest { + val resolverCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.Custom("content://media/42"), + messageSoundDisplayName = "Captured at copy time", + ), + soundDisplayNameResolver = FakeSoundDisplayNameResolver( + resolveLambda = { uri -> + resolverCalls += uri + "Live probe (should not appear)" + }, + ), + ) + presenter.test { + val state = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.messageSound.displayName == "Captured at copy time" + }.last() + assertThat(state.messageSound.displayName).isEqualTo("Captured at copy time") + // Live resolver must not be invoked when a persisted title is available. + assertThat(resolverCalls).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - SetMessageSound with Custom copies via copier and persists the FileProvider URI`() = runTest { + val recreateRecorder = lambdaRecorder { _, _ -> } + val appPreferencesStore = InMemoryAppPreferencesStore() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { sound, version -> recreateRecorder(sound, version) }, + ), + notificationSoundCopier = FakeNotificationSoundCopier(copyLambda = { _, _ -> + NotificationSoundCopier.CopyResult.Success( + fileProviderUriString = "content://my.app.fileprovider/notification_sounds/message_sound.ogg", + displayName = "Cool Tone", + ) + }), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://media/source"))) + advanceUntilIdle() + + val expectedSound = NotificationSound.Custom("content://my.app.fileprovider/notification_sounds/message_sound.ogg") + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(expectedSound) + assertThat(appPreferencesStore.getMessageSoundDisplayNameFlow().first()).isEqualTo("Cool Tone") + recreateRecorder.assertions().isCalledOnce().with(value(expectedSound), value(1)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SetMessageSound copy failure surfaces inline error and leaves prior choice intact`() = runTest { + val priorChoice = NotificationSound.Custom("content://prior") + val appPreferencesStore = InMemoryAppPreferencesStore(messageSound = priorChoice) + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundCopier = FakeNotificationSoundCopier(copyLambda = { _, _ -> + NotificationSoundCopier.CopyResult.UnplayableSource + }), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://broken"))) + + val errorState = consumeItemsUntilPredicate { it.messageSound.copyError }.last() + assertThat(errorState.messageSound.copyError).isTrue() + // Persisted state is unchanged. + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(priorChoice) + + // Dismissing the inline error clears the flag without altering persistence. + errorState.eventSink(NotificationSettingsEvents.DismissMessageSoundCopyError) + val cleared = consumeItemsUntilPredicate { !it.messageSound.copyError }.last() + assertThat(cleared.messageSound.copyError).isFalse() + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(priorChoice) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - SetCallRingtone copy failure surfaces inline error and leaves prior choice intact`() = runTest { + val priorChoice = NotificationSound.Custom("content://prior") + val appPreferencesStore = InMemoryAppPreferencesStore(callRingtone = priorChoice) + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundCopier = FakeNotificationSoundCopier(copyLambda = { _, _ -> + NotificationSoundCopier.CopyResult.UnplayableSource + }), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Custom("content://broken"))) + + val errorState = consumeItemsUntilPredicate { it.callRingtone.copyError }.last() + assertThat(errorState.callRingtone.copyError).isTrue() + // Persisted state is unchanged. + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().callRingtone).isEqualTo(priorChoice) + + // Dismissing the inline error clears the flag without altering persistence. + errorState.eventSink(NotificationSettingsEvents.DismissCallRingtoneCopyError) + val cleared = consumeItemsUntilPredicate { !it.callRingtone.copyError }.last() + assertThat(cleared.callRingtone.copyError).isFalse() + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().callRingtone).isEqualTo(priorChoice) + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - SystemDefault and Silent picks bypass the copier`() = runTest { + val copierCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { _, _ -> }, + recreateRingingCallChannelLambda = { _, _ -> }, + ), + notificationSoundCopier = FakeNotificationSoundCopier(copyLambda = { source, _ -> + copierCalls += source + NotificationSoundCopier.CopyResult.Success(source, "x") + }), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) + initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + advanceUntilIdle() + assertThat(copierCalls).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - non-Custom picks delete the stored slot file`() = runTest { + val deleteCalls = mutableListOf() + // Start from persisted Custom for both slots so the non-Custom picks below are real + // transitions (no-op short-circuit only fires when the new sound equals the persisted one). + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.Custom("content://prior-message"), + callRingtone = NotificationSound.Custom("content://prior-call"), + ), + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { _, _ -> }, + recreateRingingCallChannelLambda = { _, _ -> }, + ), + notificationSoundCopier = FakeNotificationSoundCopier( + copyLambda = { _, _ -> NotificationSoundCopier.CopyResult.Failure(IllegalStateException("not expected")) }, + deleteStoredSoundForLambda = { slot -> deleteCalls += slot }, + ), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) + initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + advanceUntilIdle() + assertThat(deleteCalls).containsExactly( + NotificationSoundCopier.SoundSlot.Message, + NotificationSoundCopier.SoundSlot.Call, + ) + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - re-selecting the persisted non-Custom sound is a no-op`() = runTest { + val recreateCalls = mutableListOf() + val deleteCalls = mutableListOf() + val copierCalls = mutableListOf() + val appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.ElementDefault, + messageSoundChannelVersion = 5, + callRingtone = NotificationSound.Silent, + callRingtoneChannelVersion = 3, + ) + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundCopier = FakeNotificationSoundCopier( + copyLambda = { source, _ -> + copierCalls += source + NotificationSoundCopier.CopyResult.Success(source, "title") + }, + deleteStoredSoundForLambda = { slot -> deleteCalls += slot }, + ), + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { _, version -> recreateCalls += version }, + recreateRingingCallChannelLambda = { _, version -> recreateCalls += version }, + ), + ) + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + + // Re-pick the persisted sound for both slots — neither should churn the channel, + // bump the version, or hit the copier / delete sweep. + initial.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementDefault)) + initial.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + advanceUntilIdle() + + val config = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(config.messageSoundVersion).isEqualTo(5) + assertThat(config.callRingtoneVersion).isEqualTo(3) + assertThat(recreateCalls).isEmpty() + assertThat(copierCalls).isEmpty() + assertThat(deleteCalls).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - rapid back-to-back picks settle in deterministic order`() = runTest { + // Defends the persist+recreate window against rapid event firing (e.g. an accessibility + // service driving the dialog). The per-slot mutex in the presenter forces copy → persist + // → recreate to complete for the first pick before the second begins, so the recreated + // version sequence matches the persisted version sequence. + val recreatedVersions = mutableListOf() + val copyOrder = mutableListOf() + val appPreferencesStore = InMemoryAppPreferencesStore() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundCopier = FakeNotificationSoundCopier(copyLambda = { source, _ -> + copyOrder += source + NotificationSoundCopier.CopyResult.Success(source, "title") + }), + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { _, version -> recreatedVersions += version }, + ), + ) + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initial.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://A"))) + initial.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://B"))) + advanceUntilIdle() + + assertThat(copyOrder).containsExactly("content://A", "content://B").inOrder() + assertThat(recreatedVersions).containsExactly(1, 2).inOrder() + val config = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(config.messageSoundVersion).isEqualTo(2) + assertThat(config.messageSound).isEqualTo(NotificationSound.Custom("content://B")) + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - Custom pick does not invoke deleteStoredSoundFor (copier sweeps inline)`() = runTest { + val deleteCalls = mutableListOf() + val presenter = createNotificationSettingsPresenter( + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { _, _ -> }, + ), + notificationSoundCopier = FakeNotificationSoundCopier( + copyLambda = { _, _ -> + NotificationSoundCopier.CopyResult.Success( + fileProviderUriString = "content://my.app.fileprovider/notification_sounds/message_sound.mp3", + displayName = "New Tone", + ) + }, + deleteStoredSoundForLambda = { slot -> deleteCalls += slot }, + ), + ) + presenter.test { + val initialState = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://media/source"))) + advanceUntilIdle() + assertThat(deleteCalls).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ShowMessageSoundDialog and DismissMessageSoundDialog toggle dialog visibility`() = runTest { + val presenter = createNotificationSettingsPresenter() + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(initial.showMessageSoundDialog).isFalse() + + initial.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) + val shown = consumeItemsUntilPredicate { it.showMessageSoundDialog }.last() + assertThat(shown.showMessageSoundDialog).isTrue() + + shown.eventSink(NotificationSettingsEvents.DismissMessageSoundDialog) + val hidden = consumeItemsUntilPredicate { !it.showMessageSoundDialog }.last() + assertThat(hidden.showMessageSoundDialog).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - SelectMessageSoundPreset persists ElementDefault, recreates the channel and closes the dialog`() = runTest { + val recreateRecorder = lambdaRecorder { _, _ -> } + val appPreferencesStore = InMemoryAppPreferencesStore() + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateNoisyChannelLambda = { sound, version -> recreateRecorder(sound, version) }, + ), + ) + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + initial.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) + val shown = consumeItemsUntilPredicate { it.showMessageSoundDialog }.last() + + shown.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementDefault)) + advanceUntilIdle() + consumeItemsUntilPredicate { + !it.showMessageSoundDialog && it.messageSound.sound == NotificationSound.ElementDefault + } + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound) + .isEqualTo(NotificationSound.ElementDefault) + recreateRecorder.assertions().isCalledOnce().with(value(NotificationSound.ElementDefault), value(1)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - LaunchMessageSoundPicker increments the picker token and closes the dialog`() = runTest { + val presenter = createNotificationSettingsPresenter() + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(initial.pendingMessageSoundPickerLaunch).isEqualTo(0) + initial.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) + val shown = consumeItemsUntilPredicate { it.showMessageSoundDialog }.last() + + shown.eventSink(NotificationSettingsEvents.LaunchMessageSoundPicker) + val launched = consumeItemsUntilPredicate { it.pendingMessageSoundPickerLaunch == 1 }.last() + assertThat(launched.showMessageSoundDialog).isFalse() + assertThat(launched.pendingMessageSoundPickerLaunch).isEqualTo(1) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - ShowCallRingtoneDialog and DismissCallRingtoneDialog toggle dialog visibility`() = runTest { + val presenter = createNotificationSettingsPresenter() + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(initial.showCallRingtoneDialog).isFalse() + + initial.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) + val shown = consumeItemsUntilPredicate { it.showCallRingtoneDialog }.last() + assertThat(shown.showCallRingtoneDialog).isTrue() + + shown.eventSink(NotificationSettingsEvents.DismissCallRingtoneDialog) + val hidden = consumeItemsUntilPredicate { !it.showCallRingtoneDialog }.last() + assertThat(hidden.showCallRingtoneDialog).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - SelectCallRingtonePreset persists SystemDefault, recreates the channel and closes the dialog`() = runTest { + val recreateRecorder = lambdaRecorder { _, _ -> } + val appPreferencesStore = InMemoryAppPreferencesStore(callRingtone = NotificationSound.Silent) + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateRingingCallChannelLambda = { sound, version -> recreateRecorder(sound, version) }, + ), + ) + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.callRingtone.sound == NotificationSound.Silent + }.last() + initial.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) + val shown = consumeItemsUntilPredicate { it.showCallRingtoneDialog }.last() + + shown.eventSink(NotificationSettingsEvents.SelectCallRingtonePreset(NotificationSound.SystemDefault)) + advanceUntilIdle() + consumeItemsUntilPredicate { + !it.showCallRingtoneDialog && it.callRingtone.sound == NotificationSound.SystemDefault + } + assertThat(appPreferencesStore.getNotificationSoundChannelConfig().callRingtone) + .isEqualTo(NotificationSound.SystemDefault) + recreateRecorder.assertions().isCalledOnce().with(value(NotificationSound.SystemDefault), value(1)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - LaunchCallRingtonePicker increments the picker token and closes the dialog`() = runTest { + val presenter = createNotificationSettingsPresenter() + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid + }.last() + assertThat(initial.pendingCallRingtonePickerLaunch).isEqualTo(0) + initial.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) + val shown = consumeItemsUntilPredicate { it.showCallRingtoneDialog }.last() + + shown.eventSink(NotificationSettingsEvents.LaunchCallRingtonePicker) + val launched = consumeItemsUntilPredicate { it.pendingCallRingtonePickerLaunch == 1 }.last() + assertThat(launched.showCallRingtoneDialog).isFalse() + assertThat(launched.pendingCallRingtonePickerLaunch).isEqualTo(1) + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - SelectCallRingtonePreset clears legacy ringtone classification`() = runTest { + // Legacy state: persisted SystemDefault + channel version 0 means the presenter probes the + // channel and surfaces whatever Android system has. We seed that probe with Silent so the + // initial row reads Silent even though DataStore holds SystemDefault. A preset pick must + // null the legacy classifier; otherwise re-reading state would still resolve to Silent + // after the SystemDefault selection persists. + val appPreferencesStore = InMemoryAppPreferencesStore( + callRingtone = NotificationSound.SystemDefault, + callRingtoneChannelVersion = 0, + ) + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = FakeNotificationSoundUpdater( + recreateRingingCallChannelLambda = { _, _ -> }, + readRingingCallChannelSoundLambda = { NotificationSound.Silent }, + ), + ) + presenter.test { + val initial = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.callRingtone.sound == NotificationSound.Silent + }.last() + initial.eventSink(NotificationSettingsEvents.SelectCallRingtonePreset(NotificationSound.SystemDefault)) + advanceUntilIdle() + val after = consumeItemsUntilPredicate { + it.callRingtone.sound == NotificationSound.SystemDefault + }.last() + assertThat(after.callRingtone.sound).isEqualTo(NotificationSound.SystemDefault) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - persisted ElementDefault renders Element default label`() = runTest { + val presenter = createNotificationSettingsPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(messageSound = NotificationSound.ElementDefault), + ) + presenter.test { + val state = consumeItemsUntilPredicate { + it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && + it.messageSound.sound == NotificationSound.ElementDefault + }.last() + // FakeStringProvider returns "A string" for any resource, so this just verifies the + // ElementDefault branch is taken (vs falling through to the Custom resolver path). + assertThat(state.messageSound.displayName).isEqualTo("A string") + cancelAndIgnoreRemainingEvents() + } + } + private fun TestScope.createNotificationSettingsPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), pushService: PushService = FakePushService(), fullScreenIntentPermissionsStateLambda: () -> FullScreenIntentPermissionsState = { aFullScreenIntentPermissionsState() }, + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + notificationSoundUpdater: NotificationSoundUpdater = FakeNotificationSoundUpdater(), + // Default copier passes the picked URI string straight through as the FileProvider URI + // and uses the URI string as the display name. Tests that exercise copy failures override. + notificationSoundCopier: NotificationSoundCopier = FakeNotificationSoundCopier(copyLambda = { source, _ -> + NotificationSoundCopier.CopyResult.Success(source, source) + }), + soundDisplayNameResolver: FakeSoundDisplayNameResolver = FakeSoundDisplayNameResolver(), ): NotificationSettingsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) return NotificationSettingsPresenter( @@ -366,6 +1126,11 @@ class NotificationSettingsPresenterTest { pushService = pushService, systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(), fullScreenIntentPermissionsPresenter = { fullScreenIntentPermissionsStateLambda() }, + appPreferencesStore = appPreferencesStore, + notificationSoundUpdater = notificationSoundUpdater, + notificationSoundCopier = notificationSoundCopier, + soundDisplayNameResolver = soundDisplayNameResolver, + stringProvider = FakeStringProvider(), sessionCoroutineScope = backgroundScope, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt index ea140abbd7..57edf01a1d 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt @@ -6,14 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.notifications import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.preferences.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -25,76 +28,70 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class NotificationSettingsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class NotificationSettingsViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onBackClick = it ) - rule.pressBack() + pressBack() } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } - @Config(qualifiers = "h1024dp") + @Config(qualifiers = "h1280dp") @Test - fun `clicking on troubleshoot notification invokes the expected callback`() { + fun `clicking on troubleshoot notification invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onTroubleshootNotificationsClick = it ) - rule.clickOn(R.string.troubleshoot_notifications_entry_point_title) + clickOn(R.string.troubleshoot_notifications_entry_point_title) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on group chats invokes the expected callback`() { + fun `clicking on group chats invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(false) { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onOpenEditDefault = it ) - rule.clickOn(R.string.screen_notification_settings_group_chats) + clickOn(R.string.screen_notification_settings_group_chats) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on direct chats invokes the expected callback`() { + fun `clicking on direct chats invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(true) { - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), onOpenEditDefault = it ) - rule.clickOn(R.string.screen_notification_settings_direct_chats) + clickOn(R.string.screen_notification_settings_direct_chats) } eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) } @@ -111,15 +108,15 @@ class NotificationSettingsViewTest { testNotificationToggle(false) } - private fun testNotificationToggle(initialState: Boolean) { + private fun testNotificationToggle(initialState: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( appNotificationEnabled = initialState, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_notification_settings_enable_notifications) + clickOn(R.string.screen_notification_settings_enable_notifications) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -140,15 +137,15 @@ class NotificationSettingsViewTest { testAtRoomToggle(false) } - private fun testAtRoomToggle(initialState: Boolean) { + private fun testAtRoomToggle(initialState: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( atRoomNotificationsEnabled = initialState, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_notification_settings_room_mention_label) + clickOn(R.string.screen_notification_settings_room_mention_label) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -169,15 +166,15 @@ class NotificationSettingsViewTest { testInvitationToggle(false) } - private fun testInvitationToggle(initialState: Boolean) { + private fun testInvitationToggle(initialState: Boolean) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( inviteForMeNotificationsEnabled = initialState, eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_notification_settings_invite_for_me_label) + clickOn(R.string.screen_notification_settings_invite_for_me_label) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -188,15 +185,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with an error configuration, clicking on continue emits the expected events`() { + fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -207,15 +204,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with invalid configuration, clicking on continue emits the expected events`() { + fun `with invalid configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = false, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -226,15 +223,15 @@ class NotificationSettingsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `with invalid configuration and error, clicking on OK emits the expected events`() { + fun `with invalid configuration and error, clicking on OK emits the expected events`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = true, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -243,16 +240,16 @@ class NotificationSettingsViewTest { ) } - @Config(qualifiers = "h1024dp") + @Config(qualifiers = "h1280dp") @Test - fun `clicking on Push notification provider emits the expected event`() { + fun `clicking on Push notification provider emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_advanced_settings_push_provider_android) + clickOn(R.string.screen_advanced_settings_push_provider_android) eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -262,16 +259,16 @@ class NotificationSettingsViewTest { } @Test - fun `clicking on a push provider emits the expected event`() { + fun `clicking on a push provider emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setNotificationSettingsView( + setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder, showChangePushProviderDialog = true, availablePushDistributors = listOf(aDistributor("P1"), aDistributor("P2")) ), ) - rule.onNodeWithText("P2").performClick() + onNodeWithText("P2").performClick() eventsRecorder.assertList( listOf( NotificationSettingsEvents.RefreshSystemNotificationsEnabled, @@ -279,9 +276,46 @@ class NotificationSettingsViewTest { ) ) } + + @Config(qualifiers = "h1280dp") + @Test + fun `sounds preference category renders rows with current display names`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder, + messageSoundDisplayName = "Pixel notification", + callRingtoneDisplayName = "Pixel ringtone", + ), + ) + onNodeWithText("Sound").assertIsDisplayed() + onNodeWithText("Message sound").assertIsDisplayed() + onNodeWithText("Call ringtone").assertIsDisplayed() + onNodeWithText("Pixel notification").assertIsDisplayed() + onNodeWithText("Pixel ringtone").assertIsDisplayed() + } + + @Config(qualifiers = "h1280dp") + @Test + fun `clicking the message sound row opens the preset dialog`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setNotificationSettingsView( + state = aValidNotificationSettingsState(eventSink = eventsRecorder), + ) + // The click now opens the in-app preset dialog instead of launching the system picker + // directly; the picker only fires from the dialog's "Choose another sound..." option + // (covered by Presenter tests + NotificationSoundPickerTest). + onNodeWithText("Message sound").performClick() + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.ShowMessageSoundDialog, + ) + ) + } } -private fun AndroidComposeTestRule.setNotificationSettingsView( +private fun AndroidComposeUiTest.setNotificationSettingsView( state: NotificationSettingsState, onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(), onTroubleshootNotificationsClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundPickerTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundPickerTest.kt new file mode 100644 index 0000000000..48f5a21eca --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundPickerTest.kt @@ -0,0 +1,88 @@ +/* + * 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.notifications + +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.provider.Settings +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test + +class NotificationSoundPickerTest : RobolectricTest() { + @Test + fun `buildRingtonePickerIntent encodes SystemDefault as the system default URI`() { + val intent = buildRingtonePickerIntent( + type = RingtoneManager.TYPE_NOTIFICATION, + current = NotificationSound.SystemDefault, + defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI, + ) + assertThat(intent.action).isEqualTo(RingtoneManager.ACTION_RINGTONE_PICKER) + assertThat(intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1)).isEqualTo(RingtoneManager.TYPE_NOTIFICATION) + assertThat(intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false)).isTrue() + assertThat(intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false)).isTrue() + @Suppress("DEPRECATION") + val existing: Uri? = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI) + assertThat(existing).isEqualTo(Settings.System.DEFAULT_NOTIFICATION_URI) + } + + @Test + fun `buildRingtonePickerIntent encodes Silent as a null existing URI`() { + val intent = buildRingtonePickerIntent( + type = RingtoneManager.TYPE_RINGTONE, + current = NotificationSound.Silent, + defaultUri = Settings.System.DEFAULT_RINGTONE_URI, + ) + @Suppress("DEPRECATION") + val existing: Uri? = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI) + assertThat(existing).isNull() + } + + @Test + fun `buildRingtonePickerIntent encodes Custom as the parsed URI`() { + val intent = buildRingtonePickerIntent( + type = RingtoneManager.TYPE_NOTIFICATION, + current = NotificationSound.Custom("content://media/internal/audio/media/42"), + defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI, + ) + @Suppress("DEPRECATION") + val existing: Uri? = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI) + assertThat(existing).isEqualTo("content://media/internal/audio/media/42".toUri()) + } + + @Test + fun `toPickedNotificationSound maps null URI to Silent`() { + val result = Intent().apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, null as Uri?) + } + assertThat(result.toPickedNotificationSound(Settings.System.DEFAULT_NOTIFICATION_URI)) + .isEqualTo(NotificationSound.Silent) + } + + @Test + fun `toPickedNotificationSound maps default URI to SystemDefault`() { + val result = Intent().apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Settings.System.DEFAULT_NOTIFICATION_URI) + } + assertThat(result.toPickedNotificationSound(Settings.System.DEFAULT_NOTIFICATION_URI)) + .isEqualTo(NotificationSound.SystemDefault) + } + + @Test + fun `toPickedNotificationSound maps any other URI to Custom`() { + val customUri = "content://media/internal/audio/media/42".toUri() + val result = Intent().apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, customUri) + } + assertThat(result.toPickedNotificationSound(Settings.System.DEFAULT_NOTIFICATION_URI)) + .isEqualTo(NotificationSound.Custom(customUri.toString())) + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundStorageTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundStorageTest.kt new file mode 100644 index 0000000000..7f8dba8226 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSoundStorageTest.kt @@ -0,0 +1,80 @@ +/* + * 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.notifications + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.preferences.api.store.NotificationSound.Companion.toStored +import org.junit.Test + +class NotificationSoundStorageTest { + @Test + fun `fromStored - null maps to SystemDefault`() { + assertThat(NotificationSound.fromStored(null)).isEqualTo(NotificationSound.SystemDefault) + } + + @Test + fun `fromStored - silent sentinel maps to Silent`() { + assertThat(NotificationSound.fromStored("silent")).isEqualTo(NotificationSound.Silent) + } + + @Test + fun `fromStored - element_default sentinel maps to ElementDefault`() { + assertThat(NotificationSound.fromStored("element_default")).isEqualTo(NotificationSound.ElementDefault) + } + + @Test + fun `fromStored - element_fade sentinel maps to ElementFade`() { + assertThat(NotificationSound.fromStored("element_fade")).isEqualTo(NotificationSound.ElementFade) + } + + @Test + fun `fromStored - any other string is treated as a Custom URI`() { + assertThat(NotificationSound.fromStored("content://media/42")) + .isEqualTo(NotificationSound.Custom("content://media/42")) + } + + @Test + fun `toStored - SystemDefault encodes as null`() { + assertThat(NotificationSound.SystemDefault.toStored()).isNull() + } + + @Test + fun `toStored - ElementDefault encodes as element_default`() { + assertThat(NotificationSound.ElementDefault.toStored()).isEqualTo("element_default") + } + + @Test + fun `toStored - ElementFade encodes as element_fade`() { + assertThat(NotificationSound.ElementFade.toStored()).isEqualTo("element_fade") + } + + @Test + fun `toStored - Silent encodes as silent`() { + assertThat(NotificationSound.Silent.toStored()).isEqualTo("silent") + } + + @Test + fun `toStored - Custom encodes as the URI string`() { + assertThat(NotificationSound.Custom("content://x").toStored()).isEqualTo("content://x") + } + + @Test + fun `round-trip preserves every variant`() { + val variants = listOf( + NotificationSound.SystemDefault, + NotificationSound.ElementDefault, + NotificationSound.ElementFade, + NotificationSound.Silent, + NotificationSound.Custom("content://media/42"), + ) + for (sound in variants) { + assertThat(NotificationSound.fromStored(sound.toStored())).isEqualTo(sound) + } + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index e5d48ec175..a6861be12d 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -12,6 +12,8 @@ package io.element.android.features.preferences.impl.root import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.SessionEnterpriseService +import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.rageshake.api.RageshakeFeatureAvailability @@ -23,7 +25,7 @@ import io.element.android.libraries.featureflag.test.FakeFeature import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.indicator.test.FakeIndicatorService -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -65,6 +67,9 @@ class PreferencesRootPresenterTest { ) createPresenter( matrixClient = matrixClient, + sessionEnterpriseService = FakeSessionEnterpriseService( + tweakMasUrlResult = { "tweaked $it" }, + ), ).test { val initialState = awaitItem() assertThat(initialState.myUser).isEqualTo( @@ -100,7 +105,7 @@ class PreferencesRootPresenterTest { val finalState = awaitItem() accountManagementUrlResult.assertions().isCalledOnce() .with(value(null)) - assertThat(finalState.accountManagementUrl).isEqualTo("null url") + assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url") } } @@ -327,6 +332,7 @@ class PreferencesRootPresenterTest { indicatorService: IndicatorService = FakeIndicatorService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), sessionStore: SessionStore = InMemorySessionStore(), + sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(), ) = PreferencesRootPresenter( matrixClient = matrixClient, sessionVerificationService = sessionVerificationService, @@ -339,5 +345,6 @@ class PreferencesRootPresenterTest { rageshakeFeatureAvailability = rageshakeFeatureAvailability, featureFlagService = featureFlagService, sessionStore = sessionStore, + sessionEnterpriseService = sessionEnterpriseService, ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt index da91bdbf86..aded097059 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootViewTest.kt @@ -5,14 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.preferences.impl.R import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.A_USER_ID_2 @@ -25,49 +27,44 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PreferencesRootViewTest { - @get:Rule val rule = createAndroidComposeRule() +class PreferencesRootViewTest : RobolectricTest() { @Test - fun `clicking on back invokes back callback`() { + fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder ), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `click on User profile invokes the expected callback`() { + fun `click on User profile invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val user = aMatrixUser() ensureCalledOnceWithParam(user) { callback -> - rule.setView( + setView( aPreferencesRootState( myUser = user, eventSink = eventsRecorder, ), onOpenUserProfile = callback, ) - rule.onNodeWithText("Alice").performClick() + onNodeWithText("Alice").performClick() } } @Test - fun `clicking on other session sends a SwitchToSession`() { + fun `clicking on other session sends a SwitchToSession`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setView( + setView( aPreferencesRootState( isMultiAccountEnabled = true, otherSessions = listOf( @@ -79,366 +76,366 @@ class PreferencesRootViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText("Bob").performClick() + onNodeWithText("Bob").performClick() eventsRecorder.assertSingle(PreferencesRootEvent.SwitchToSession(A_USER_ID_2)) } @Test - fun `click on Add account invokes the expected callback`() { + fun `click on Add account invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( isMultiAccountEnabled = true, eventSink = eventsRecorder, ), onAddAccountClick = callback, ) - rule.clickOn(CommonStrings.common_add_another_account) + clickOn(CommonStrings.common_add_another_account) } } @Test - fun `when multi account is not enabled, item is not shown`() { + fun `when multi account is not enabled, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( isMultiAccountEnabled = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_add_another_account)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_add_another_account)).assertDoesNotExist() } @Test - fun `click on Encryption invokes the expected callback`() { + fun `click on Encryption invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showSecureBackup = true, eventSink = eventsRecorder, ), onSecureBackupClick = callback, ) - rule.clickOn(CommonStrings.common_encryption) + clickOn(CommonStrings.common_encryption) } } @Test - fun `when showSecureBackup is false, item is not shown`() { + fun `when showSecureBackup is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showSecureBackup = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_encryption)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_encryption)).assertDoesNotExist() } @Test - fun `click on Manage account invokes the expected callback`() { + fun `click on Manage account invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam("aUrl") { callback -> - rule.setView( + setView( aPreferencesRootState( accountManagementUrl = "aUrl", eventSink = eventsRecorder, ), onManageAccountClick = callback, ) - rule.clickOn(CommonStrings.action_manage_account_and_devices) + clickOn(CommonStrings.action_manage_account_and_devices) } } @Test - fun `when accountManagementUrl is null, item is not shown`() { + fun `when accountManagementUrl is null, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( accountManagementUrl = null, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.action_manage_account_and_devices)).assertDoesNotExist() } @Test - fun `click on Link new devices invokes the expected callback`() { + fun `click on Link new devices invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showLinkNewDevice = true, eventSink = eventsRecorder, ), onLinkNewDeviceClick = callback, ) - rule.clickOn(CommonStrings.common_link_new_device) + clickOn(CommonStrings.common_link_new_device) } } @Test - fun `when showLinkNewDevice is false, item is not shown`() { + fun `when showLinkNewDevice is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showLinkNewDevice = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_link_new_device)).assertDoesNotExist() } @Test - fun `click on Analytics invokes the expected callback`() { + fun `click on Analytics invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showAnalyticsSettings = true, eventSink = eventsRecorder, ), onOpenAnalytics = callback, ) - rule.clickOn(CommonStrings.common_analytics) + clickOn(CommonStrings.common_analytics) } } @Test - fun `when showAnalyticsSettings is false, item is not shown`() { + fun `when showAnalyticsSettings is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showAnalyticsSettings = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_analytics)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_analytics)).assertDoesNotExist() } @Test - fun `click on Report a problem invokes the expected callback`() { + fun `click on Report a problem invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( canReportBug = true, eventSink = eventsRecorder, ), onOpenRageShake = callback, ) - rule.clickOn(CommonStrings.common_report_a_problem) + clickOn(CommonStrings.common_report_a_problem) } } @Test - fun `when canReportBug is false, item is not shown`() { + fun `when canReportBug is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( canReportBug = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_report_a_problem)).assertDoesNotExist() } @Test - fun `click on Screen lock invokes the expected callback`() { + fun `click on Screen lock invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenLockScreenSettings = callback, ) - rule.clickOn(CommonStrings.common_screen_lock) + clickOn(CommonStrings.common_screen_lock) } } @Test - fun `click on About invokes the expected callback`() { + fun `click on About invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenAbout = callback, ) - rule.clickOn(CommonStrings.common_about) + clickOn(CommonStrings.common_about) } } @Test - fun `click on Developer settings invokes the expected callback`() { + fun `click on Developer settings invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showDeveloperSettings = true, eventSink = eventsRecorder, ), onOpenDeveloperSettings = callback, ) - rule.clickOn(CommonStrings.common_developer_options) + clickOn(CommonStrings.common_developer_options) } } @Test - fun `when showDeveloperSettings is false, item is not shown`() { + fun `when showDeveloperSettings is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showDeveloperSettings = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_developer_options)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_developer_options)).assertDoesNotExist() } @Test - fun `click on Advanced settings invokes the expected callback`() { + fun `click on Advanced settings invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenAdvancedSettings = callback, ) - rule.clickOn(CommonStrings.common_advanced_settings) + clickOn(CommonStrings.common_advanced_settings) } } @Test - fun `click on Labs invokes the expected callback`() { + fun `click on Labs invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( showLabsItem = true, eventSink = eventsRecorder, ), onOpenLabs = callback, ) - rule.clickOn(R.string.screen_labs_title) + clickOn(R.string.screen_labs_title) } } @Test - fun `when showLabsItem is false, item is not shown`() { + fun `when showLabsItem is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( showLabsItem = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(R.string.screen_labs_title)).assertDoesNotExist() + onNodeWithText(activity!!.getString(R.string.screen_labs_title)).assertDoesNotExist() } @Test - fun `click on Notification invokes the expected callback`() { + fun `click on Notification invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onOpenNotificationSettings = callback, ) - rule.clickOn(R.string.screen_notification_settings_title) + clickOn(R.string.screen_notification_settings_title) } } @Test - fun `click on Blocked users invokes the expected callback`() { + fun `click on Blocked users invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( nbOfBlockedUsers = 1, eventSink = eventsRecorder, ), onOpenBlockedUsers = callback, ) - rule.clickOn(CommonStrings.common_blocked_users) + clickOn(CommonStrings.common_blocked_users) } } @Test - fun `when nbOfBlockedUsers is 0, item is not shown`() { + fun `when nbOfBlockedUsers is 0, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( nbOfBlockedUsers = 0, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.common_blocked_users)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.common_blocked_users)).assertDoesNotExist() } @Test - fun `click on Remove this device invokes the expected callback`() { + fun `click on Remove this device invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( eventSink = eventsRecorder, ), onSignOutClick = callback, ) - rule.clickOn(CommonStrings.action_signout) + clickOn(CommonStrings.action_signout) } } @Test - fun `click on Deactivate invokes the expected callback`() { + fun `click on Deactivate invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setView( + setView( aPreferencesRootState( canDeactivateAccount = true, eventSink = eventsRecorder, ), onDeactivateClick = callback, ) - rule.clickOn(CommonStrings.action_delete_account) + clickOn(CommonStrings.action_delete_account) } } @Test - fun `when canDeactivateAccount is false, item is not shown`() { + fun `when canDeactivateAccount is false, item is not shown`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setView( + setView( aPreferencesRootState( canDeactivateAccount = false, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_delete_account)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.action_delete_account)).assertDoesNotExist() } @Test - fun `clicking on version sends a PreferencesRootEvents`() { + fun `clicking on version sends a PreferencesRootEvents`() = runAndroidComposeUiTest { val version = "VERSION" val eventsRecorder = EventsRecorder() - rule.setView( + setView( aPreferencesRootState( version = version, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(version).performClick() + onNodeWithText(version).performClick() eventsRecorder.assertSingle(PreferencesRootEvent.OnVersionInfoClick) } } -private fun AndroidComposeTestRule.setView( +private fun AndroidComposeUiTest.setView( state: PreferencesRootState, onBackClick: () -> Unit = EnsureNeverCalled(), onAddAccountClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt index 6845ecb3a4..ed1eb4255b 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt @@ -19,19 +19,19 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.push.test.FakePushService +import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore +import io.element.android.libraries.sessionstorage.test.aCacheData import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import okhttp3.OkHttpClient import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultClearCacheUseCaseTest { +class DefaultClearCacheUseCaseTest : RobolectricTest() { @Test fun `execute clear cache should do all the expected tasks`() = runTest { val activeRoomsHolder = DefaultActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) } @@ -49,6 +49,9 @@ class DefaultClearCacheUseCaseTest { ) val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID)) assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty() + val cacheStore = InMemoryCacheStore( + initialData = mapOf("key1" to aCacheData()) + ) val sut = DefaultClearCacheUseCase( context = InstrumentationRegistry.getInstrumentation().context, matrixClient = matrixClient, @@ -58,9 +61,11 @@ class DefaultClearCacheUseCaseTest { pushService = pushService, seenInvitesStore = seenInvitesStore, activeRoomsHolder = activeRoomsHolder, + cacheStore = cacheStore, ) defaultCacheService.clearedCacheEventFlow.test { sut.invoke() + assertThat(cacheStore.dataMap).isEmpty() clearCacheLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() .with(value(matrixClient.sessionId), value(false)) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt index 728e05ee7e..a9bce23e45 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileViewTest.kt @@ -6,15 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.preferences.impl.user.editprofile import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi 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 androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.ui.strings.CommonStrings @@ -23,96 +25,92 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class EditUserProfileViewTest { - @get:Rule val rule = createAndroidComposeRule() +class EditUserProfileViewTest : RobolectricTest() { @Test - fun `clicking on back emits the expected event`() { + fun `clicking on back emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( eventSink = eventsRecorder, ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(EditUserProfileEvent.Exit) } @Test - fun `clicking on save from the exit confirmation dialog emits the expected event`() { + fun `clicking on save from the exit confirmation dialog emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save, inDialog = true) + clickOn(CommonStrings.action_save, inDialog = true) eventsRecorder.assertSingle(EditUserProfileEvent.Save) } @Test - fun `clicking on discard exit emits the expected event`() { + fun `clicking on discard exit emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(EditUserProfileEvent.Exit) } @Test - fun `clicking on save emits the expected event`() { + fun `clicking on save emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveButtonEnabled = true, saveAction = AsyncAction.Uninitialized, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(EditUserProfileEvent.Save) } @Test - fun `clicking on avatar opens the bottom sheet dialog`() { + fun `clicking on avatar opens the bottom sheet dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val actions = listOf( AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove, ) - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.Uninitialized, avatarActions = actions, eventSink = eventsRecorder, ), ) - val contentDescription = rule.activity.getString(CommonStrings.a11y_avatar) - rule.onNodeWithContentDescription(contentDescription).performClick() + val resources = activity!!.resources + val contentDescription = resources.getString(CommonStrings.a11y_avatar) + onNodeWithContentDescription(contentDescription).performClick() // Assert that the actions are displayed actions.forEach { action -> - val text = rule.activity.getString(action.titleResId) - rule.onNodeWithText(text).assertExists() + val text = resources.getString(action.titleResId) + onNodeWithText(text).assertExists() } } @Test - fun `success invokes the expected callback`() { + fun `success invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setEditUserProfileView( + setEditUserProfileView( aEditUserProfileState( saveAction = AsyncAction.Success(Unit), eventSink = eventsRecorder, @@ -123,7 +121,7 @@ class EditUserProfileViewTest { } } -private fun AndroidComposeTestRule.setEditUserProfileView( +private fun AndroidComposeUiTest.setEditUserProfileView( state: EditUserProfileState, onEditProfileSuccess: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/preferences/test/build.gradle.kts b/features/preferences/test/build.gradle.kts index 7e3da4a6e8..a066fe4707 100644 --- a/features/preferences/test/build.gradle.kts +++ b/features/preferences/test/build.gradle.kts @@ -14,6 +14,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.features.preferences.api) implementation(projects.tests.testutils) } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt index 5db1d4f076..ea78ed5a2c 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDetectionView.kt @@ -43,7 +43,7 @@ private fun CrashDetectionContent( onDismiss: () -> Unit = { }, ) { ConfirmationDialog( - title = stringResource(id = CommonStrings.action_report_bug), + title = stringResource(id = CommonStrings.common_report_a_problem), content = stringResource(id = R.string.crash_detection_dialog_content, appName), submitText = stringResource(id = CommonStrings.action_yes), cancelText = stringResource(id = CommonStrings.action_no), diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt index 745a362637..c60bd4e1bf 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/detection/RageshakeDetectionView.kt @@ -77,7 +77,7 @@ private fun RageshakeDialogContent( onYesClick: () -> Unit = { }, ) { ConfirmationDialog( - title = stringResource(id = CommonStrings.action_report_bug), + title = stringResource(id = CommonStrings.common_report_a_problem), content = stringResource(id = R.string.rageshake_detection_dialog_content), thirdButtonText = stringResource(id = CommonStrings.action_disable), submitText = stringResource(id = CommonStrings.action_yes), diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt index 79b05ea32e..631a51a686 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt @@ -20,6 +20,7 @@ interface BugReporter { * @param problemDescription the bug description * @param canContact true if the user opt in to be contacted directly * @param sendPushRules true to include the push rules + * @param ghIssueNumber it not null, the GitHub issue number to link the bug report to. * @param listener the listener */ suspend fun sendBugReport( @@ -29,6 +30,7 @@ interface BugReporter { problemDescription: String, canContact: Boolean = false, sendPushRules: Boolean = false, + ghIssueNumber: Int? = null, listener: BugReporterListener ) diff --git a/features/rageshake/api/src/main/res/values-be/translations.xml b/features/rageshake/api/src/main/res/values-be/translations.xml index 3d9eec29d2..f08697c71d 100644 --- a/features/rageshake/api/src/main/res/values-be/translations.xml +++ b/features/rageshake/api/src/main/res/values-be/translations.xml @@ -1,6 +1,6 @@ - "Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?" + "Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце абагуліць справаздачу аб збоі?" "Падобна, што вы трасеце тэлефон. Хочаце адкрыць экран паведамлення пра памылку?" "Rageshake" "Парог выяўлення" diff --git a/features/rageshake/api/src/main/res/values-ca/translations.xml b/features/rageshake/api/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..b5907369e6 --- /dev/null +++ b/features/rageshake/api/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "%1$s ha fallat l\'últim cop que es va utilitzar. Vols enviar un informe d\'errors?" + "Sembla que sacseges el telèfon amb frustració. Vols obrir la pantalla d\'informe d\'errors?" + "La sacsejada de frustració és quan mous i sacseges el mòbil per informar d\'un error. L\'aplicació ja no és compatible amb aquesta funcionalitat però el nom encara s\'utilitza per raons històriques." + "Llindar de detecció" + diff --git a/features/rageshake/api/src/main/res/values-ru/translations.xml b/features/rageshake/api/src/main/res/values-ru/translations.xml index 8a3df584d7..c0448b7498 100644 --- a/features/rageshake/api/src/main/res/values-ru/translations.xml +++ b/features/rageshake/api/src/main/res/values-ru/translations.xml @@ -1,7 +1,7 @@ "При последнем использовании %1$s произошел сбой. Хотите отправить отчет?" - "Похоже, что вы трясете телефон. Хотите открыть экран сообщения об ошибке?" + "Похоже, что Вы трясете телефон в раздражении. Хотите открыть экран сообщения об ошибке?" "Встряхните" "Порог обнаружения" diff --git a/features/rageshake/api/src/main/res/values-zh/translations.xml b/features/rageshake/api/src/main/res/values-zh/translations.xml index ca81dd2d2c..8c78bbbc9c 100644 --- a/features/rageshake/api/src/main/res/values-zh/translations.xml +++ b/features/rageshake/api/src/main/res/values-zh/translations.xml @@ -1,6 +1,6 @@ - "%1$s 上次使用时崩溃了。想和我们分享崩溃报告吗?" + "%1$s 上次使用时曾崩溃过。是否与我们分享崩溃报告?" "你似乎愤怒地摇晃了手机。是否打开 Bug 报告页面?" "摇一摇" "检测阈值" diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt index 90751873dd..94897d1bad 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt @@ -18,4 +18,5 @@ sealed interface BugReportEvents { data class SetCanContact(val canContact: Boolean) : BugReportEvents data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents + data class SetGhIssueNumber(val ghIssueNumber: Int?) : BugReportEvents } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt index 4985f9b30e..85731ae959 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt @@ -113,6 +113,9 @@ class BugReportPresenter( sendingProgress.floatValue = 0f sendingAction.value = AsyncAction.Uninitialized } + is BugReportEvents.SetGhIssueNumber -> updateFormState(formState) { + copy(ghIssueNumber = event.ghIssueNumber) + } } } @@ -142,7 +145,8 @@ class BugReportPresenter( problemDescription = formState.description, canContact = formState.canContact, sendPushRules = formState.sendPushRules, - listener = listener + ghIssueNumber = formState.ghIssueNumber, + listener = listener, ) } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt index 65cc055ec9..8e07b3c4e8 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt @@ -32,6 +32,7 @@ data class BugReportFormState( val canContact: Boolean, val sendScreenshot: Boolean, val sendPushRules: Boolean, + val ghIssueNumber: Int?, ) : Parcelable { companion object { val Default = BugReportFormState( @@ -40,6 +41,7 @@ data class BugReportFormState( canContact = false, sendScreenshot = false, sendPushRules = false, + ghIssueNumber = null, ) } } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt index c2c34835c9..36b407a669 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.request.CachePolicy import coil3.request.ImageRequest +import io.element.android.compound.theme.ElementTheme import io.element.android.features.rageshake.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.async.AsyncActionView @@ -155,6 +156,48 @@ fun BugReportView( title = stringResource(R.string.screen_bug_report_send_notification_settings_title), subtitle = stringResource(R.string.screen_bug_report_send_notification_settings_description), ) + PreferenceRow { + var ghIssueNumberState by textFieldState( + stateValue = state.formState.ghIssueNumber?.toString() ?: "" + ) + TextField( + value = ghIssueNumberState, + modifier = Modifier + .fillMaxWidth() + .onTabOrEnterKeyFocusNext(LocalFocusManager.current), + enabled = isFormEnabled, + label = stringResource(id = R.string.screen_bug_report_github_issue_label), + placeholder = "1234", + supportingText = stringResource(id = R.string.screen_bug_report_github_issue_description), + leadingIcon = { + Text( + text = "#", + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textSecondary, + ) + }, + onValueChange = { + if (it.isEmpty()) { + ghIssueNumberState = "" + eventSink(BugReportEvents.SetGhIssueNumber(null)) + } else { + val number = it.toIntOrNull()?.takeIf { ghInt -> ghInt in 1..99_999 } + number?.let { ghIssueNumber -> + ghIssueNumberState = ghIssueNumber.toString() + eventSink(BugReportEvents.SetGhIssueNumber(ghIssueNumber)) + } + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next, + ), + keyboardActions = KeyboardActions(onNext = { + keyboardController?.hide() + }), + singleLine = true, + ) + } // Submit PreferenceRow { Button( diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index b4e7faa01e..735bb1cea0 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -14,7 +14,6 @@ import androidx.core.net.toFile import androidx.core.net.toUri import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Provider import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.RageshakeConfig import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration @@ -77,7 +76,7 @@ class DefaultBugReporter( private val screenshotHolder: ScreenshotHolder, private val crashDataStore: CrashDataStore, private val coroutineDispatchers: CoroutineDispatchers, - private val okHttpClient: Provider, + private val okHttpClient: () -> OkHttpClient, private val userAgentProvider: UserAgentProvider, private val sessionStore: SessionStore, private val buildMeta: BuildMeta, @@ -124,6 +123,7 @@ class DefaultBugReporter( problemDescription: String, canContact: Boolean, sendPushRules: Boolean, + ghIssueNumber: Int?, listener: BugReporterListener, ) { val url = bugReporterUrlProvider.provide().first() @@ -145,6 +145,9 @@ class DefaultBugReporter( val crashCallStack = crashDataStore.crashInfo().first() val bugDescription = buildString { append(problemDescription) + ghIssueNumber?.let { + append("\n\nhttps://github.com/element-hq/element-x-android/issues/$it") + } if (crashCallStack.isNotEmpty() && withCrashLogs) { append("\n\n\n\n--------------------------------- crash call stack ---------------------------------\n") append(crashCallStack) diff --git a/features/rageshake/impl/src/main/res/values-be/translations.xml b/features/rageshake/impl/src/main/res/values-be/translations.xml index f927ba3c5e..50f8bb6052 100644 --- a/features/rageshake/impl/src/main/res/values-be/translations.xml +++ b/features/rageshake/impl/src/main/res/values-be/translations.xml @@ -12,6 +12,6 @@ "Дазволіць журналы" "Адправіць здымак экрана" "Каб пераканацца, што ўсё працуе правільна, у паведамленне будуць уключаны часопісы. Каб адправіць паведамленне без часопісаў, адключыце гэтую наладу." - "Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?" + "Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце абагуліць справаздачу аб збоі?" "Прагляд журналаў" diff --git a/features/rageshake/impl/src/main/res/values-ca/translations.xml b/features/rageshake/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..6586cd9dc2 --- /dev/null +++ b/features/rageshake/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,17 @@ + + + "Adjunta captura de pantalla" + "Pots contactar amb mi si tens comentaris addicionals." + "Contacta\'m" + "Edita captura de pantalla" + "Descriu el problema. Què has fet? Què esperaves que passés? Què va has passat realment? Si us plau, explica-ho el més detalladament possible." + "Descriu el problema…" + "Si és possible, escriu la descripció en anglès." + "Descripció massa curta. Si us plau, proporciona més detalls sobre què ha passat. Gràcies!" + "Envia registre de fallada" + "Permet registres" + "Envia captura de pantalla" + "Els registres s\'inclouran amb el missatge per intentar resoldre el problema correctament. Per enviar el missatge sense registres, desactiva aquesta opció." + "%1$s ha fallat l\'últim cop que es va utilitzar. Vols enviar un informe d\'errors?" + "Veure registres" + diff --git a/features/rageshake/impl/src/main/res/values-et/translations.xml b/features/rageshake/impl/src/main/res/values-et/translations.xml index 2f55a9b6b5..1f61849fa5 100644 --- a/features/rageshake/impl/src/main/res/values-et/translations.xml +++ b/features/rageshake/impl/src/main/res/values-et/translations.xml @@ -8,6 +8,8 @@ "Palun kirjelda probleemi…" "Kui vähegi võimalik, siis kirjuta inglise keeles." "Kirjeldus on liiga lühike. Palun jaga täpsemat teavet selle kohta, mis juhtus. Tänud juba ette!" + "Võid sisestada seotud GitHubi veateate numbri, kui see on olemas." + "Veateade GitHubis" "Saada krahhilogid" "Luba logide saatmine" "Sinu logid on väga mahukad ja neid ei saa siia lisada. Palun saada logid meile mõnel muul viisil." diff --git a/features/rageshake/impl/src/main/res/values-fa/translations.xml b/features/rageshake/impl/src/main/res/values-fa/translations.xml index ba5ab756db..2e27f97ba6 100644 --- a/features/rageshake/impl/src/main/res/values-fa/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fa/translations.xml @@ -7,6 +7,7 @@ "لطفاً مشکل را شرح دهید. چه‌کار کردید؟ انتظار داشتید چه بشود؟ ولی چه شد؟ لطفاً‌تا جای ممکن وارد جزییات شوید." "شرح مشکل…" "ترجیحاً توضیحات را به زبان انگلیسی بنویسید." + "توضیحات خیلی کوتاه است، لطفاً جزئیات بیشتری در مورد آنچه اتفاق افتاده ارائه دهید. متشکرم!" "ارسال رخدادنگارهای خطا" "اجازه به گزارش‌ها" "ارسال تصویر صفحه" diff --git a/features/rageshake/impl/src/main/res/values-fi/translations.xml b/features/rageshake/impl/src/main/res/values-fi/translations.xml index 1970d538fd..5590602367 100644 --- a/features/rageshake/impl/src/main/res/values-fi/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fi/translations.xml @@ -8,6 +8,8 @@ "Kuvaile ongelmaasi…" "Jos mahdollista, kirjoita englanniksi." "Kuvaus on liian lyhyt. Kerro tarkemmin mitä tapahtui, kiitos!" + "Voit kirjoittaa mahdollisen GitHub-issuen numeron." + "GitHub-issue" "Lähetä kaatumislokit" "Lähetä lokitiedostot" "Lokitiedostosi ovat liian suuria, joten niitä ei voida sisällyttää tähän raporttiin. Lähetä ne meille toisella tavalla." diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml index 6685903146..7f0e3c0e54 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -8,6 +8,8 @@ "Décrire le problème…" "Si possible, veuillez rédiger la description en anglais." "La description est trop courte, veuillez fournir plus de détails sur ce qui s’est passé. Merci !" + "Vous pouvez saisir le numéro d’un ticket GitHub associé, le cas échéant." + "Ticket GitHub" "Envoyer des journaux d’incident" "Autoriser à inclure les journaux techniques" "Vos fichiers de log sont trop volumineux et ne peuvent donc pas être inclus dans ce rapport, veuillez nous les envoyer par un autre moyen." diff --git a/features/rageshake/impl/src/main/res/values-hu/translations.xml b/features/rageshake/impl/src/main/res/values-hu/translations.xml index 851d3f0067..db665a1253 100644 --- a/features/rageshake/impl/src/main/res/values-hu/translations.xml +++ b/features/rageshake/impl/src/main/res/values-hu/translations.xml @@ -8,6 +8,8 @@ "Írja le a problémát…" "Ha lehetséges, a leírást angolul írja meg." "A leírás túl rövid, adjon meg további részleteket a történtekről. Köszönjük!" + "Megadhatja egy kapcsolódó GitHub-probléma számát, ha van ilyen." + "GitHub-jegy" "Összeomlásnaplók küldése" "Naplók engedélyezése" "A naplófájlok túl nagyok, ezért nem szerepelhetnek ebben a jelentésben. Más módon küldje el őket." diff --git a/features/rageshake/impl/src/main/res/values-ja/translations.xml b/features/rageshake/impl/src/main/res/values-ja/translations.xml index 7c46d2beb5..53d25b364f 100644 --- a/features/rageshake/impl/src/main/res/values-ja/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ja/translations.xml @@ -8,6 +8,8 @@ "問題の説明…" "可能であれば英語で記入してください。" "説明が過度に短いです。問題についてより詳細にご記入ください。" + "関連したGitHubのissueがある場合はIDを記入してください。" + "GitHub issue" "クラッシュログを送信" "ログの記録を許可" "ログのサイズが大きく、報告に添付することができません。ログは別の方法で送信してください。" diff --git a/features/rageshake/impl/src/main/res/values-ko/translations.xml b/features/rageshake/impl/src/main/res/values-ko/translations.xml index ef4978924c..1b15ce3f5a 100644 --- a/features/rageshake/impl/src/main/res/values-ko/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ko/translations.xml @@ -8,6 +8,8 @@ "문제를 설명해 주세요…" "가능하다면 영어로 설명을 작성해 주십시오." "설명 내용이 너무 짧습니다. 발생한 상황에 대해 더 자세한 내용을 제공해 주시기 바랍니다. 감사합니다!" + "연관된 GitHub 이슈 번호가 있다면 입력할 수 있습니다." + "GitHub 이슈" "충돌 로그 보내기" "로그 허용" "귀하의 로그가 너무 커서 이 보고서에 포함할 수 없습니다. 다른 방법으로 보내주시기 바랍니다." diff --git a/features/rageshake/impl/src/main/res/values-pl/translations.xml b/features/rageshake/impl/src/main/res/values-pl/translations.xml index c433071584..2b0feb0851 100644 --- a/features/rageshake/impl/src/main/res/values-pl/translations.xml +++ b/features/rageshake/impl/src/main/res/values-pl/translations.xml @@ -8,6 +8,8 @@ "Opisz problem…" "Jeśli to możliwe, napisz zgłoszenje w języku angielskim." "Opis jest zbyt krótki, podaj więcej szczegółów na temat tego co się stało. Dzięki!" + "Możesz wpisać numer powiązanego zgłoszenia w serwisie GitHub, jeśli takie istnieje." + "Zgłoszenie Github" "Wyślij logi awarii" "Zezwól na logi" "Twoje dzienniki są zbyt duże, więc nie można ich uwzględnić w tym raporcie. Prześlij je do nas w inny sposób." diff --git a/features/rageshake/impl/src/main/res/values-ru/translations.xml b/features/rageshake/impl/src/main/res/values-ru/translations.xml index d4224a6dcb..4934266798 100644 --- a/features/rageshake/impl/src/main/res/values-ru/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ru/translations.xml @@ -8,13 +8,15 @@ "Опишите проблему…" "Если возможно, пожалуйста, напишите описание на английском языке." "Описание слишком короткое, пожалуйста, расскажите подробнее о том, что произошло. Спасибо!" + "При наличии, Вы можете ввести номер связанной проблемы из GitHub." + "Проблема на GitHub" "Отправка журналов сбоев" "Разрешить ведение журналов" "Ваши журналы слишком большие для включения в этот отчет. Пожалуйста, отправьте их нам другим способом." "Отправить скриншот" "Чтобы убедиться, что всё работает правильно, к сообщению будут прикреплены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку." "При последнем использовании %1$s произошел сбой. Хотите отправить отчет?" - "Если у вас возникли проблемы с уведомлениями, загрузка настроек уведомлений может помочь нам определить основную причину." + "Если у Вас возникли проблемы с уведомлениями, загрузка настроек уведомлений может помочь нам определить причину проблемы. Обратите внимание, что эти правила могут содержать конфиденциальную информацию, такую ​​как Ваше отображаемое имя или ключевые слова, по которым должны срабатывать уведомления." "Настройки отправки уведомлений" "Просмотр журналов" diff --git a/features/rageshake/impl/src/main/res/values-uk/translations.xml b/features/rageshake/impl/src/main/res/values-uk/translations.xml index 3999133537..d42f6e5501 100644 --- a/features/rageshake/impl/src/main/res/values-uk/translations.xml +++ b/features/rageshake/impl/src/main/res/values-uk/translations.xml @@ -8,11 +8,15 @@ "Опишіть проблему…" "Якщо можливо, будь ласка, напишіть опис англійською мовою." "Опис закороткий, будь ласка, надайте докладнішу інформацію про те, що сталося. Дякуємо!" + "Ви можете ввести номер вже існуючої проблеми на GitHub, якщо така є." + "Тікет на GitHub" "Надіслати журнали збоїв" "Дозволити журнали" "Ваші журнали надмірно великі, тому їх не можна включити в цей звіт, будь ласка, надішліть їх нам іншим способом." "Надіслати знімок екрана" "Журнали будуть додані до вашого повідомлення, щоб переконатися, що все працює належним чином. Щоб надіслати повідомлення без журналів, вимкніть це налаштування." "Стався збій %1$s під час останнього користування. Хочете поділитися з нами звітом про збій?" + "Якщо у вас виникають проблеми зі сповіщеннями, надсилання нам правил push-сповіщень допоможе нам визначити першопричину. Зверніть увагу, що ці правила можуть містити приватну інформацію, таку як ваше ім’я користувача або ключові слова, за якими ви отримували сповіщення." + "Налаштування сповіщень" "Переглянути журнали" diff --git a/features/rageshake/impl/src/main/res/values-zh/translations.xml b/features/rageshake/impl/src/main/res/values-zh/translations.xml index b34badea08..17cac7de3b 100644 --- a/features/rageshake/impl/src/main/res/values-zh/translations.xml +++ b/features/rageshake/impl/src/main/res/values-zh/translations.xml @@ -8,12 +8,13 @@ "描述问题…" "请尽可能用英文描述。" "描述太短,请提供详细情况。谢谢!" + "你可以输入与之相关的 GitHub issue 编号(如果有)。" "发送崩溃日志" "允许日志" "日志文件过大,无法包含在本次报告中,请通过其它方式发送给我们。" "发送屏幕截图" "为确认一切正常运行,日志中将包含你的消息。如要发送不含消息的日志,请关闭此设置。" - "%1$s 上次使用时崩溃了。想和我们分享崩溃报告吗?" + "%1$s 上次使用时曾崩溃过。是否与我们分享崩溃报告?" "如果你遭遇通知相关问题,上传通知设置可以帮助我们调查根本原因。请注意:这些规则可能包含私人信息,例如你的显示名称或用于接收通知的关键词。" "发送通知设置" "查看日志" diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index 85af2af4a7..d1dc52f3e4 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -8,6 +8,8 @@ "Describe the problem…" "If possible, please write the description in English." "The description is too short, please provide more details about what happened. Thanks!" + "You can enter the number of an associated GitHub issue, if any." + "GitHub issue" "Send crash logs" "Allow logs" "Your logs are excessively large so cannot be included in this report, please send them to us another way." diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index 148c375e25..afbcc939a6 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -107,6 +107,18 @@ class BugReportPresenterTest { } } + @Test + fun `present - set GitHub issue number`() = runTest { + val presenter = createPresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink.invoke(BugReportEvents.SetGhIssueNumber(1)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(ghIssueNumber = 1)) + initialState.eventSink.invoke(BugReportEvents.SetGhIssueNumber(null)) + assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(ghIssueNumber = null)) + } + } + @Test fun `present - reset all`() = runTest { val presenter = createPresenter( diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt index 36cc185e86..659145caa9 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt @@ -28,6 +28,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter { problemDescription: String, canContact: Boolean, sendPushRules: Boolean, + ghIssueNumber: Int?, listener: BugReporterListener, ) { delay(100) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt index 665e1ce4bd..70abe99469 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt @@ -11,14 +11,12 @@ package io.element.android.features.rageshake.impl.crash import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class VectorUncaughtExceptionHandlerTest { +class VectorUncaughtExceptionHandlerTest : RobolectricTest() { @Test fun `activate should change the default handler`() { val sut = VectorUncaughtExceptionHandler(PreferencesCrashDataStore(FakePreferenceDataStoreFactory())) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index 6dd6f81e5f..4f666d374b 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -32,6 +32,7 @@ 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.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -46,12 +47,9 @@ import okhttp3.mockwebserver.RecordedRequest import okio.buffer import okio.source import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -@RunWith(RobolectricTestRunner::class) -class DefaultBugReporterTest { +class DefaultBugReporterTest : RobolectricTest() { @Test fun `test sendBugReport success`() = runTest { val server = MockWebServer() diff --git a/features/reportroom/impl/src/main/res/values-ca/translations.xml b/features/reportroom/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..05ca5c0fc0 --- /dev/null +++ b/features/reportroom/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,6 @@ + + + "Denuncia aquesta sala al teu administrador. Si els missatges estan xifrats, l\'administrador no els podrà llegir." + "Descriu el motiu de la denúncia…" + "Denuncia sala" + diff --git a/features/reportroom/impl/src/main/res/values-ru/translations.xml b/features/reportroom/impl/src/main/res/values-ru/translations.xml index 24a46b5f3d..22ff15c1cd 100644 --- a/features/reportroom/impl/src/main/res/values-ru/translations.xml +++ b/features/reportroom/impl/src/main/res/values-ru/translations.xml @@ -1,6 +1,6 @@ - "Ваша жалоба была успешно отправлена, но мы столкнулись с проблемой при попытке покинуть комнату. Пожалуйста, попробуйте еще раз." + "Ваша жалоба была успешно отправлена, но мы столкнулись с проблемой при попытке покинуть комнату. Пожалуйста, попробуйте ещё раз." "Невозможно покинуть комнату" "Пожалуйтесь на комнату администратору. Если сообщения зашифрованы, ваш администратор не сможет их прочитать." "Опишите причину жалобы…" diff --git a/features/reportroom/impl/src/main/res/values-zh/translations.xml b/features/reportroom/impl/src/main/res/values-zh/translations.xml index e79148a45d..4366e33455 100644 --- a/features/reportroom/impl/src/main/res/values-zh/translations.xml +++ b/features/reportroom/impl/src/main/res/values-zh/translations.xml @@ -2,7 +2,7 @@ "你的举报已成功提交,但在尝试退出房间时遇到问题。请重试。" "无法离开房间" - "向管理员举报此房间。如果信息已加密,管理员将无法读取。" + "向管理员举报此房间。如果消息已加密,管理员将无法读取。" "描述举报的理由…" "举报房间" diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt index 59d9507571..0e43cf6594 100644 --- a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt @@ -6,90 +6,87 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.reportroom.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ReportRoomViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ReportRoomViewTest : RobolectricTest() { @Test - fun `clicking on back invoke the expected callback`() { + fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setReportRoomView( + setReportRoomView( aReportRoomState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on report when enabled emits the expected event`() { + fun `clicking on report when enabled emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setReportRoomView( + setReportRoomView( aReportRoomState( reason = "Spam", eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_report) + clickOn(CommonStrings.action_report) eventsRecorder.assertSingle(ReportRoomEvents.Report) } @Test - fun `clicking on decline when disabled does not emit event`() { + fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setReportRoomView( + setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_report) + clickOn(CommonStrings.action_report) } @Test - fun `clicking on leave room option emits the expected event`() { + fun `clicking on leave room option emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setReportRoomView( + setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_leave_room) + clickOn(CommonStrings.action_leave_room) eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) } @Test - fun `typing text in the reason field emits the expected Event`() { + fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setReportRoomView( + setReportRoomView( aReportRoomState( eventSink = eventsRecorder, reason = "" ), ) - rule.onNodeWithText("").performTextInput("Spam!") + onNodeWithText("").performTextInput("Spam!") eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) } } -private fun AndroidComposeTestRule.setReportRoomView( +private fun AndroidComposeUiTest.setReportRoomView( state: ReportRoomState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt index 552f1d0ec6..3023124e9e 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenter.kt @@ -47,6 +47,7 @@ class ChangeRoomPermissionsPresenter( RoomPermissionsSection.MessagesAndContent -> persistentListOf( RoomPermissionType.SEND_EVENTS, RoomPermissionType.REDACT_EVENTS, + RoomPermissionType.SHARE_LIVE_LOCATION, ) RoomPermissionsSection.ManageMembers -> persistentListOf( RoomPermissionType.INVITE, @@ -114,6 +115,7 @@ class ChangeRoomPermissionsPresenter( RoomPermissionType.ROOM_AVATAR -> currentPermissions?.copy(roomAvatar = powerLevel) RoomPermissionType.ROOM_TOPIC -> currentPermissions?.copy(roomTopic = powerLevel) RoomPermissionType.SPACE_MANAGE_ROOMS -> currentPermissions?.copy(spaceChild = powerLevel) + RoomPermissionType.SHARE_LIVE_LOCATION -> currentPermissions?.copy(beaconInfo = powerLevel, beacon = powerLevel) } } is ChangeRoomPermissionsEvent.Save -> coroutineScope.save() diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt index 535f2b4a71..b6b2ca0bda 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsState.kt @@ -65,6 +65,7 @@ data class ChangeRoomPermissionsState( RoomPermissionType.ROOM_AVATAR -> currentPermissions.roomAvatar RoomPermissionType.ROOM_TOPIC -> currentPermissions.roomTopic RoomPermissionType.SPACE_MANAGE_ROOMS -> currentPermissions.spaceChild + RoomPermissionType.SHARE_LIVE_LOCATION -> maxOf(currentPermissions.beacon, currentPermissions.beaconInfo) } } } @@ -104,4 +105,5 @@ enum class RoomPermissionType { ROOM_AVATAR, ROOM_TOPIC, SPACE_MANAGE_ROOMS, + SHARE_LIVE_LOCATION, } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt index 2760272d8a..700a82795e 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt @@ -63,5 +63,8 @@ private fun previewPermissions(): RoomPowerLevelsValues { // SpaceManagement section spaceChild = RoomMember.Role.Moderator.powerLevel, stateDefault = RoomMember.Role.Moderator.powerLevel, + // Live location beacon section + beacon = RoomMember.Role.Admin.powerLevel, + beaconInfo = RoomMember.Role.Moderator.powerLevel, ) } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt index 529df0d50d..bb702ed90a 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt @@ -127,6 +127,7 @@ private fun titleForType(type: RoomPermissionType): String = when (type) { RoomPermissionType.ROOM_AVATAR -> stringResource(R.string.screen_room_change_permissions_room_avatar) RoomPermissionType.ROOM_TOPIC -> stringResource(R.string.screen_room_change_permissions_room_topic) RoomPermissionType.SPACE_MANAGE_ROOMS -> stringResource(R.string.screen_room_change_permissions_manage_space_rooms) + RoomPermissionType.SHARE_LIVE_LOCATION -> stringResource(R.string.screen_room_change_permissions_live_location) } @PreviewsDayNight diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt index ebaf619d4e..1ea1662a99 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt @@ -11,6 +11,10 @@ package io.element.android.features.rolesandpermissions.impl.roles import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember @@ -103,9 +107,9 @@ internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState( internal fun aChangeRolesStateWithOwners( role: RoomMember.Role = RoomMember.Role.Admin, selectedUsers: List = listOf( - aMatrixUser(id = "@alice:server.org", displayName = "Alice"), - aMatrixUser(id = "@bob:server.org", displayName = "Bob"), - aMatrixUser(id = "@carol:server.org", displayName = "Carol"), + aMatrixUser(displayName = USER_NAME_ALICE), + aMatrixUser(displayName = USER_NAME_BOB), + aMatrixUser(displayName = USER_NAME_CAROL), ), ) = aChangeRolesState( role = role, @@ -114,22 +118,22 @@ internal fun aChangeRolesStateWithOwners( members = persistentListOf( aRoomMember( userId = UserId("@alice:server.org"), - displayName = "Alice", + displayName = USER_NAME_ALICE, role = RoomMember.Role.Owner(isCreator = true), ), aRoomMember( userId = UserId("@bob:server.org"), - displayName = "Bob", + displayName = USER_NAME_BOB, role = RoomMember.Role.Owner(isCreator = false), ), aRoomMember( userId = UserId("@carol:server.org"), - displayName = "Carol", + displayName = USER_NAME_CAROL, role = RoomMember.Role.Admin, ), aRoomMember( userId = UserId("@david:server.org"), - displayName = "David", + displayName = USER_NAME_DAVID, role = RoomMember.Role.User, ), ), diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt index da69ee52a9..050027bb1c 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsNode.kt @@ -26,12 +26,8 @@ class RolesAndPermissionsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: RolesAndPermissionsPresenter, -) : Node(buildContext, plugins = plugins), RolesAndPermissionsNavigator { +) : Node(buildContext, plugins = plugins) { interface Callback : Plugin, RolesAndPermissionsNavigator { - override fun openAdminList() - override fun openModeratorList() - override fun openEditPermissions() - override fun onBackClick() {} } @@ -56,8 +52,8 @@ class RolesAndPermissionsNode( } interface RolesAndPermissionsNavigator { - fun onBackClick() {} - fun openAdminList() {} - fun openModeratorList() {} - fun openEditPermissions() {} + fun onBackClick() + fun openAdminList() + fun openModeratorList() + fun openEditPermissions() } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt index 2f8dab8029..67092afb75 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt @@ -181,7 +181,6 @@ private fun ChangeOwnRoleBottomSheet( ListItem( headlineContent = { Text(stringResource(CommonStrings.action_cancel)) }, onClick = ::dismiss, - style = ListItemStyle.Primary, ) } } @@ -192,7 +191,12 @@ internal fun RolesAndPermissionsViewPreview(@PreviewParameter(RolesAndPermission ElementPreview { RolesAndPermissionsView( state = state, - rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator {}, + rolesAndPermissionsNavigator = object : RolesAndPermissionsNavigator { + override fun onBackClick() {} + override fun openAdminList() {} + override fun openModeratorList() {} + override fun openEditPermissions() {} + }, ) } } diff --git a/features/rolesandpermissions/impl/src/main/res/values-be/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-be/translations.xml index 44841d1207..4e4ff9bb58 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-be/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-be/translations.xml @@ -1,11 +1,12 @@ - "Толькі адміністратары" + "Адміністратар" "Заблакіраваць людзей" "Выдаліць паведамленні" - "Запрашайце людзей і прымайце запыты на далучэнне" + "Удзельнік" + "Запрасіць людзей" "Паведамленні і змест" - "Адміністратары і мадэратары" + "Мадэратар" "Выдаляйце людзей і адхіляйце запыты на далучэнне" "Змяніць аватар пакоя" "Рэдагаваць пакой" @@ -39,8 +40,8 @@ "Яны змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." "Заблакіраваныя" "Удзельнікі" - "Толькі адміністратары" - "Адміністратары і мадэратары" + "Адміністратар" + "Мадэратар" "Удзельнікі пакоя" "Разблакіроўка %1$s" "Адміністратары" diff --git a/features/rolesandpermissions/impl/src/main/res/values-ca/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..eccea6f2df --- /dev/null +++ b/features/rolesandpermissions/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,61 @@ + + + "Només administradors" + "Bandejar usuaris" + "Eliminar missatges" + "Membre" + "Convidar persones i acceptar sol·licituds d\'unió" + "Gestiona membres" + "Missatges i contingut" + "Administradors i moderadors" + "Eliminar persones i rebutjar sol·licituds d\'unió" + "Canvia la foto de la sala" + "Edita detalls" + "Canvia el nom de la sala" + "Canvia el tema de la sala" + "Enviar missatges" + "Edita administradors" + "No podràs desfer aquesta acció. Estàs concedint a l\'usuari el mateix nivell de poder que tu." + "Afegir com a administrador?" + "Descendeix" + "No podràs desfer aquest canvi ja que t\'estàs descendint de rang, si ets l\'últim usuari de la sala amb privilegis, no podràs recuperar-los." + "Vols descendir de categoria?" + "%1$s (pendent)" + "(pendent)" + "Els administradors tenen automàticament privilegis de moderador" + "Edita moderadors" + "Administradors" + "Moderadors" + "Membres" + "Hi ha canvis sense desar." + "Desar canvis?" + "No hi ha usuaris bandejats en aquesta sala." + + "%1$d persona" + "%1$d persones" + + "Bandeja de la sala" + "Només elimina\'l" + "Desbandeja" + "Podran tornar a unir-se a aquesta sala si se\'ls convida." + "Readmet usuari" + "Bandejats" + "Membres" + "Només administradors" + "Administradors i moderadors" + "Membres de la sala" + "Readmetent %1$s" + "Administradors" + "Canvia el meu rol" + "Descendeix a membre" + "Descendeix a moderador" + "Moderació de membres" + "Missatges i contingut" + "Moderadors" + "Restableix permisos" + "Si restableixes els permisos, perdràs la configuració actual." + "Restablir permisos?" + "Rols" + "Detalls de sala" + "Rols i permisos" + diff --git a/features/rolesandpermissions/impl/src/main/res/values-cs/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-cs/translations.xml index 87936de3a7..662096dd20 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-cs/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-cs/translations.xml @@ -6,6 +6,7 @@ "Odstranit zprávy" "Člen" "Pozvat přátele" + "Sdílet aktuální polohu" "Správa prostoru" "Spravovat místnosti" "Spravovat členy" diff --git a/features/rolesandpermissions/impl/src/main/res/values-et/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-et/translations.xml index 1c1f490580..02ec4ad07d 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-et/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-et/translations.xml @@ -6,6 +6,7 @@ "Eemalda sõnumid" "Liikmed" "Osalejate kutsumine" + "Jaga asukohta reaalajas" "Halda kogukonda" "Halda jututuba" "Liikmete haldus" diff --git a/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml index c526d059bc..4bcb764219 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-fa/translations.xml @@ -1,16 +1,16 @@ - "فقط مدیران" + "ادمین" "تحریم افراد" - "برداشتن پیام‌ها" - "هرکسی" - "دعوت افراد و پذیرش درخواست‌های پیوستن" - "نظارت اعضا" + "حذف پیام‌ها" + "عضو" + "دعوت کاربران" + "مدیریت اعضا" "پیام‌ها و محتوا" - "مدیرن و ناظران" - "برداشتن افراد و رد درخواست‌های پیوستن" + "ناظم" + "حذف افراد" "تغییر چهرک اتاق" - "ویرایش اتاق" + "ویرایش جزییات" "تغییر نام اتاق" "دگرگونی موضوع اتاق" "فرستادن پیام‌ها" @@ -44,8 +44,8 @@ "تحریم نکردن از اتاق" "محروم" "اعضا" - "فقط مدیران" - "مدیرن و ناظران" + "ادمین" + "ناظم" "مالک" "اعضای اتاق" "رفع تحریم %1$s" @@ -62,5 +62,5 @@ "بازنشانی اجازه‌ها؟" "نقش‌ها" "جزییات اتاق" - "نقش‌ها و اجازه‌ها" + "نقش‌ها و مجوزها" diff --git a/features/rolesandpermissions/impl/src/main/res/values-fi/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-fi/translations.xml index 7c5d635b14..fdc1c17f22 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-fi/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-fi/translations.xml @@ -6,6 +6,7 @@ "Viestien poistaminen" "Jäsen" "Kutsujen antaminen" + "Jaa reaaliaikainen sijainti" "Tilan hallitseminen" "Huoneiden hallitseminen" "Jäsenien hallitseminen" diff --git a/features/rolesandpermissions/impl/src/main/res/values-fr/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-fr/translations.xml index 7e5736bbed..677a4ba33b 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-fr/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-fr/translations.xml @@ -6,6 +6,7 @@ "Supprimer des messages" "Membre" "Inviter des personnes" + "Partager la position en continu" "Gérer l’espace" "Gérer les salons" "Gérer les membres" diff --git a/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml index e44a1dba21..9c88a2f20e 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-hu/translations.xml @@ -6,6 +6,7 @@ "Üzenetek eltávolítása" "Tag" "Emberek meghívása" + "Valós idejű hely megosztása" "Tér kezelése" "Szobák kezelése" "Tagok kezelése" diff --git a/features/rolesandpermissions/impl/src/main/res/values-it/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-it/translations.xml index b1dea12151..ac1366d988 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-it/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-it/translations.xml @@ -6,6 +6,7 @@ "Rimuovi messaggi" "Membro" "Invita persone" + "Condividi posizione in tempo reale" "Gestire lo spazio" "Gestisci le stanze" "Gestisci membri" diff --git a/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml index c9410e9e3c..28e1a9db89 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ja/translations.xml @@ -6,6 +6,7 @@ "メッセージの削除" "メンバー" "ユーザーの招待" + "ライブ位置情報を共有" "スペースの管理" "ルームを管理" "メンバーの管理" diff --git a/features/rolesandpermissions/impl/src/main/res/values-ko/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ko/translations.xml index 4ad64f1723..c9de19a43a 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ko/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ko/translations.xml @@ -6,6 +6,7 @@ "메시지 삭제" "멤버" "사람 초대하기" + "실시간 위치 공유" "스페이스 관리" "방 관리" "멤버 관리" diff --git a/features/rolesandpermissions/impl/src/main/res/values-pl/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-pl/translations.xml index 44659c47cd..9b570f5261 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-pl/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-pl/translations.xml @@ -1,17 +1,24 @@ - "Tylko administratorzy" + "Administrator" "Banowanie osób" + "Zmień ustawienia" "Usuń wiadomości" - "Zapraszanie osób i akceptowanie próśb o dołączenie" + "Członek" + "Zaproś osoby" + "Udostępnij lokalizację na żywo" + "Zarządzaj przestrzeniami" + "Zarządzaj pokojami" + "Zarządzaj członkami" "Wiadomości i zawartość" - "Administratorzy i moderatorzy" - "Usuwanie osób i odrzucanie próśb o dołączenie" + "Moderator" + "Usuń osoby" "Zmień awatar pokoju" - "Edytuj pokój" + "Edytuj szczegóły" "Zmień nazwę pokoju" "Zmień temat pokoju" "Wysyłanie wiadomości" + "Uprawnienia" "Edytuj administratorów" "Tej akcji nie będzie można cofnąć. Promujesz użytkownika, który będzie posiadał takie same uprawnienia jak Ty." "Dodać administratora?" @@ -21,7 +28,7 @@ "Nie będzie można cofnąć tej zmiany, jeśli się zdegradujesz. Jeśli jesteś ostatnim uprzywilejowanym użytkownikiem w pokoju, nie będziesz w stanie odzyskać uprawnień." "Zdegradować siebie?" "%1$s (Oczekujące)" - "(Oczekujący)" + "(Oczekujące)" "Administratorzy automatycznie mają uprawnienia moderatora" "Właściciele automatycznie mają uprawnienia administratora." "Edytuj moderatorów" @@ -31,7 +38,14 @@ "Członków" "Masz niezapisane zmiany." "Zapisać zmiany?" - "W tym pokoju nie ma zbanowanych użytkowników." + "Nie ma zbanowanych użytkowników." + + "%1$d zbanowany" + "%1$d zbanowanych" + "%1$d zbanowanych" + + "Sprawdź pisownię lub wyszukaj ponownie" + "Brak wyników dla “%1$s”" "%1$d osoba" "%1$d osoby" @@ -44,8 +58,14 @@ "Odbanuj z pokoju" "Zbanowanych" "Członków" - "Tylko administratorzy" - "Administratorzy i moderatorzy" + + "%1$d zaproszony" + "%1$d zaproszonych" + "%1$d zaproszonych" + + "Oczekuje" + "Administrator" + "Moderator" "Właściciel" "Członkowie pokoju" "Odbanowanie %1$s" @@ -58,10 +78,12 @@ "Wiadomości i zawartość" "Moderatorzy" "Właściciele" - "Resetuj uprawnienia" + "Uprawnienia" + "Zresetuj uprawnienia" "Po zresetowaniu uprawnień utracisz bieżące ustawienia." "Zresetować uprawnienia?" "Role" "Szczegóły pokoju" + "Szczegóły przestrzeni" "Role i uprawnienia" diff --git a/features/rolesandpermissions/impl/src/main/res/values-ro/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ro/translations.xml index ac9ec710a2..962f8b2d4d 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ro/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ro/translations.xml @@ -6,6 +6,7 @@ "Ștergeți mesajele" "Membru" "Invitați persoane" + "Partajați locația în timp real" "Gestionați spațiul" "Gestionați camerele" "Gestionați membrii" diff --git a/features/rolesandpermissions/impl/src/main/res/values-ru/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-ru/translations.xml index 2b7fca0254..8ec7d7579c 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-ru/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-ru/translations.xml @@ -6,6 +6,7 @@ "Удалять сообщения" "Участники" "Приглашать людей" + "Поделиться текущим местоположением" "Управление пространством" "Управление комнатами" "Управлять участниками" @@ -19,7 +20,7 @@ "Отправлять сообщения" "Разрешения" "Редактировать администраторов" - "Вы не сможете отменить это действие. Вы даете пользователю такой же уровень прав, как и у вас" + "Вы не сможете отменить это действие. Вы даете пользователю такой же уровень прав, как и у Вас" "Добавить администратора?" "Отменить данное действие будет невозможно. Права передадутся выбранным пользователям. После вашего выхода действие станет необратимым." "Передать владение?" @@ -35,7 +36,7 @@ "Администраторы" "Модераторы" "Участники" - "У вас есть несохраненные изменения." + "У Вас есть несохраненные изменения." "Сохранить изменения?" "В этой комнате нет заблокированных пользователей." diff --git a/features/rolesandpermissions/impl/src/main/res/values-uk/translations.xml b/features/rolesandpermissions/impl/src/main/res/values-uk/translations.xml index 7ce3e78387..5690886c60 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-uk/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-uk/translations.xml @@ -6,6 +6,7 @@ "Вилучати повідомлення" "Учасник" "Запрошувати людей" + "Ділитися місцезнаходженням у реальному часі" "Керувати простором" "Керувати кімнатами" "Керувати учасниками" @@ -38,6 +39,11 @@ "У вас є не збережені зміни." "Зберегти зміни?" "Немає заблокованих користувачів." + + "%1$d Заблокований" + "%1$d Заблоковано" + "%1$d Заблоковано" + "Перевірте правопис або спробуйте новий пошук" "Немає результатів за запитом «%1$s»" 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 9d792fcc74..53edd50a6d 100644 --- a/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml +++ b/features/rolesandpermissions/impl/src/main/res/values-zh/translations.xml @@ -1,11 +1,12 @@ "管理员" - "封禁成员" + "封禁人员" "更改设置" "移除消息" "成员" "邀请人员" + "共享实时位置" "管理空间" "管理房间" "管理成员" @@ -39,7 +40,7 @@ "保存更改?" "暂无被封禁的用户。" - "%1$d 被禁用" + "%1$d 人被封禁" "检查拼写或尝试新搜索" "未找到 “%1$s” 相关结果" diff --git a/features/rolesandpermissions/impl/src/main/res/values/localazy.xml b/features/rolesandpermissions/impl/src/main/res/values/localazy.xml index e5ab3f1cd7..dc89095786 100644 --- a/features/rolesandpermissions/impl/src/main/res/values/localazy.xml +++ b/features/rolesandpermissions/impl/src/main/res/values/localazy.xml @@ -6,6 +6,7 @@ "Remove messages" "Member" "Invite people" + "Share live location" "Manage space" "Manage rooms" "Manage members" diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt index ba7d47adb2..c143b7c969 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsPresenterTest.kt @@ -66,6 +66,7 @@ class ChangeRoomPermissionsPresenterTest { assertThat(itemsBySection[RoomPermissionsSection.MessagesAndContent]).containsExactly( RoomPermissionType.SEND_EVENTS, RoomPermissionType.REDACT_EVENTS, + RoomPermissionType.SHARE_LIVE_LOCATION, ) assertThat(itemsBySection[RoomPermissionsSection.ManageMembers]).containsExactly( RoomPermissionType.INVITE, @@ -148,7 +149,9 @@ class ChangeRoomPermissionsPresenterTest { roomName = Moderator.powerLevel, roomAvatar = Moderator.powerLevel, roomTopic = Moderator.powerLevel, - spaceChild = initialPermissions.spaceChild + spaceChild = initialPermissions.spaceChild, + beacon = initialPermissions.beacon, + beaconInfo = initialPermissions.beaconInfo, ) ) } diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt index f28c9c150f..57307706e0 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.rolesandpermissions.impl.permissions 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.rolesandpermissions.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings @@ -21,86 +23,81 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ChangeRoomPermissionsViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ChangeRoomPermissionsViewTest : RobolectricTest() { @Test - fun `click on back icon invokes Exit`() { + fun `click on back icon invokes Exit`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( eventSink = recorder ) ) - rule.pressBack() + pressBack() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `click on back key invokes Exit`() { + fun `click on back key invokes Exit`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( eventSink = recorder ) ) - rule.pressBackKey() + pressBackKey() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, using the back key actually exits`() { + fun `when confirming exit with pending changes, using the back key actually exits`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, eventSink = recorder, ), ) - rule.pressBackKey() + pressBackKey() recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() { + fun `when confirming exit with pending changes, clicking on 'discard' button in the dialog actually exits`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) recorder.assertSingle(ChangeRoomPermissionsEvent.Exit) } @Test - fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() { + fun `when confirming exit with pending changes, clicking on 'save' button in the dialog saves the changes`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_save, inDialog = true) + clickOn(CommonStrings.action_save, inDialog = true) recorder.assertSingle(ChangeRoomPermissionsEvent.Save) } @Test - fun `click on a role item triggers ChangeRole event`() { + fun `click on a role item triggers ChangeRole event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( itemsBySection = persistentMapOf( // Makes sure there is only one item to click on @@ -109,70 +106,70 @@ class ChangeRoomPermissionsViewTest { eventSink = recorder, ) ) - rule.clickOn(R.string.screen_room_change_permissions_room_name) - rule.clickOn(R.string.screen_room_change_permissions_everyone) + clickOn(R.string.screen_room_change_permissions_room_name) + clickOn(R.string.screen_room_change_permissions_everyone) recorder.assertSingle( ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(RoomPermissionType.ROOM_NAME, SelectableRole.Everyone), ) } @Test - fun `click on the Save menu item triggers Save event`() { + fun `click on the Save menu item triggers Save event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) recorder.assertSingle(ChangeRoomPermissionsEvent.Save) } @Test - fun `a successful save exits the screen`() { + fun `a successful save exits the screen`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(true) { callback -> - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Success(true), ), onComplete = callback, ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) } } @Test - fun `a cancellation exits the screen`() { + fun `a cancellation exits the screen`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(false) { callback -> - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Success(false), ), onComplete = callback, ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) } } @Test - fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() { + fun `click on the Ok option in save error dialog triggers ResetPendingAction event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setChangeRoomPermissionsRule( + setChangeRoomPermissionsRule( state = aChangeRoomPermissionsState( hasChanges = true, saveAction = AsyncAction.Failure(IllegalStateException("Failed to set room power levels")), eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) recorder.assertSingle(ChangeRoomPermissionsEvent.ResetPendingActions) } } -private fun AndroidComposeTestRule.setChangeRoomPermissionsRule( +private fun AndroidComposeUiTest.setChangeRoomPermissionsRule( state: ChangeRoomPermissionsState = aChangeRoomPermissionsState(), onComplete: (Boolean) -> Unit = EnsureNeverCalledWithParam(), ) { diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt index 09bef49cbd..dfc2b88e25 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesViewTest.kt @@ -6,16 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.rolesandpermissions.impl.roles import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onAllNodesWithText 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 androidx.compose.ui.test.v2.runAndroidComposeUiTest import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -29,21 +31,16 @@ import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.toImmutableList -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 ChangeRolesViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class ChangeRolesViewTest : RobolectricTest() { @Test - fun `passing a 'User' role throws an exception`() { + fun `passing a 'User' role throws an exception`() = runAndroidComposeUiTest { val exception = runCatchingExceptions { - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.User, eventSink = EnsureNeverCalledWithParam(), @@ -54,106 +51,106 @@ class ChangeRolesViewTest { } @Test - fun `back key - with search active toggles the search`() { + fun `back key - with search active toggles the search`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, eventSink = eventsRecorder, ), ) - rule.pressBackKey() + pressBackKey() // Advance time to let the event be processed, as the search toggle might have some delay (e.g. for the animation) - rule.mainClock.advanceTimeBy(1) + mainClock.advanceTimeBy(1) eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive) } @Test - fun `back key - with search inactive exits the screen`() { + fun `back key - with search inactive exits the screen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = false, eventSink = eventsRecorder, ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `back button - exits the screen`() { + fun `back button - exits the screen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = false, eventSink = eventsRecorder, ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `save button - with changes, it saves them`() { + fun `save button - with changes, it saves them`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( hasPendingChanges = true, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save button - with no changes, does nothing`() { + fun `save button - with no changes, does nothing`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( hasPendingChanges = false, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertEmpty() } @Test - fun `exit confirmation dialog - discard exits the screen`() { + fun `exit confirmation dialog - discard exits the screen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(ChangeRolesEvent.Exit) } @Test - fun `exit confirmation dialog - save emits the save event`() { + fun `exit confirmation dialog - save emits the save event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save admins confirmation dialog - submit saves the changes`() { + fun `save admins confirmation dialog - submit saves the changes`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Admin, isSearchActive = true, @@ -161,14 +158,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save owners confirmation dialog - continue saves the changes`() { + fun `save owners confirmation dialog - continue saves the changes`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Owner(isCreator = false), isSearchActive = true, @@ -176,14 +173,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(ChangeRolesEvent.Save) } @Test - fun `save admins confirmation dialog - cancel removes the dialog`() { + fun `save admins confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Admin, isSearchActive = true, @@ -191,14 +188,14 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `save owners confirmation dialog - cancel removes the dialog`() { + fun `save owners confirmation dialog - cancel removes the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( role = RoomMember.Role.Owner(isCreator = false), isSearchActive = true, @@ -206,39 +203,39 @@ class ChangeRolesViewTest { eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `error dialog - dismissing removes the dialog`() { + fun `error dialog - dismissing removes the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesState( isSearchActive = true, savingState = AsyncAction.Failure(IllegalStateException("boom")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) } @Test - fun `testing removing user from selected list emits the expected event`() { + fun `testing removing user from selected list emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val userToDeselect = selectedUsers[1] assertThat(userToDeselect.displayName).isEqualTo("Bob") - rule.setChangeRolesContent( + setChangeRolesContent( state = aChangeRolesStateWithSelectedUsers().copy( selectedUsers = selectedUsers.toImmutableList(), eventSink = eventsRecorder, ), ) // Unselect the user from the row list - val contentDescription = rule.activity.getString(CommonStrings.action_remove) - rule.onNodeWithContentDescription( + val contentDescription = activity!!.getString(CommonStrings.action_remove) + onNodeWithContentDescription( label = contentDescription, useUnmergedTree = true, ).performClick() @@ -247,7 +244,7 @@ class ChangeRolesViewTest { @Test @Config(qualifiers = "h1000dp") - fun `testing adding user to the selected list emits the expected event`() { + fun `testing adding user to the selected list emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val state = aChangeRolesStateWithSelectedUsers().copy( @@ -256,16 +253,16 @@ class ChangeRolesViewTest { ) val userToSelect = (state.searchResults as SearchBarResultState.Results).results.members.first().toMatrixUser() assertThat(userToSelect.displayName).isEqualTo("Carol") - rule.setChangeRolesContent( + setChangeRolesContent( state = state, ) // Select the user from the user list - rule.onNodeWithText("Carol").performClick() + onNodeWithText("Carol").performClick() eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } @Test - fun `testing removing user to the selected list emits the expected event`() { + fun `testing removing user to the selected list emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val selectedUsers = aMatrixUserList().take(2) val state = aChangeRolesStateWithSelectedUsers().copy( @@ -274,18 +271,18 @@ class ChangeRolesViewTest { ) val userToSelect = (state.searchResults as SearchBarResultState.Results).results.moderators.first().toMatrixUser() assertThat(userToSelect.displayName).isEqualTo("Bob") - rule.setChangeRolesContent( + setChangeRolesContent( state = state, ) // Unselect the user from the user list - rule.onAllNodesWithText( + onAllNodesWithText( text = "Bob", useUnmergedTree = true, )[1].performClick() eventsRecorder.assertSingle(ChangeRolesEvent.UserSelectionToggled(userToSelect)) } - private fun AndroidComposeTestRule.setChangeRolesContent( + private fun AndroidComposeUiTest.setChangeRolesContent( state: ChangeRolesState, ) { setContent { diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/DefaultChangeRoomMemberRolesEntyPointTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/DefaultChangeRoomMemberRolesEntyPointTest.kt index 7d190b85f4..8add944927 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/DefaultChangeRoomMemberRolesEntyPointTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/roles/DefaultChangeRoomMemberRolesEntyPointTest.kt @@ -8,18 +8,16 @@ package io.element.android.features.rolesandpermissions.impl.roles -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.tests.testutils.node.TestParentNode +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultChangeRoomMemberRolesEntyPointTest { +class DefaultChangeRoomMemberRolesEntyPointTest : RobolectricTest() { @Test fun `test node builder`() = runTest { val entryPoint = DefaultChangeRoomMemberRolesEntyPoint() diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt index e08ae205b7..8a3b6fd766 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.rolesandpermissions.impl.root 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.rolesandpermissions.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.RoomMember @@ -22,160 +24,154 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledTimes import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import kotlinx.coroutines.test.runTest -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 RolesAndPermissionsViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class RolesAndPermissionsViewTest : RobolectricTest() { @Test - fun `click on back invokes expected callback`() { + fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( goBack = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `tapping on Admins opens admin list`() { + fun `tapping on Admins opens admin list`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( aRolesAndPermissionsState( roomSupportsOwners = false, eventSink = EventsRecorder(expectEvents = false) ), openAdminList = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_admins) + clickOn(R.string.screen_room_roles_and_permissions_admins) } } @Test - fun `tapping on Admins and Owners opens admin list`() { + fun `tapping on Admins and Owners opens admin list`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( aRolesAndPermissionsState( roomSupportsOwners = true, eventSink = EventsRecorder(expectEvents = false) ), openAdminList = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) + clickOn(R.string.screen_room_roles_and_permissions_admins_and_owners) } } @Test - fun `tapping on Moderators opens moderators list`() { + fun `tapping on Moderators opens moderators list`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( openModeratorList = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_moderators) + clickOn(R.string.screen_room_roles_and_permissions_moderators) } } @Test @Config(qualifiers = "h640dp") - fun `tapping permission item open the change permissions screen`() { + fun `tapping permission item open the change permissions screen`() = runAndroidComposeUiTest { ensureCalledTimes(1) { callback -> - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( openEditPermissions = callback, ) - rule.clickOn(R.string.screen_room_roles_and_permissions_permissions_header) + clickOn(R.string.screen_room_roles_and_permissions_permissions_header) } } @Test @Config(qualifiers = "h640dp") - fun `tapping on reset permissions triggers ResetPermissions event`() { + fun `tapping on reset permissions triggers ResetPermissions event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( eventSink = recorder, ), ) - rule.clickOn(R.string.screen_room_roles_and_permissions_reset) + clickOn(R.string.screen_room_roles_and_permissions_reset) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) } @Test - fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() { + fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_reset) + clickOn(CommonStrings.action_reset) recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) } @Test - fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() { + fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) } @Test - fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() { + fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) - rule.mainClock.advanceTimeBy(1_000L) + clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) + mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator)) } @Test - fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runTest { + fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) - rule.mainClock.advanceTimeBy(1_000L) + clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) + mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User)) } @Test - fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() { + fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setRolesAndPermissionsView( + setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, eventSink = recorder, ), ) - rule.clickOn(CommonStrings.action_cancel) - rule.mainClock.advanceTimeBy(1_000L) + clickOn(CommonStrings.action_cancel) + mainClock.advanceTimeBy(1_000L) recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) } } -private fun AndroidComposeTestRule.setRolesAndPermissionsView( +private fun AndroidComposeUiTest.setRolesAndPermissionsView( state: RolesAndPermissionsState = aRolesAndPermissionsState( roomSupportsOwners = false, eventSink = EventsRecorder(expectEvents = false), diff --git a/features/roomaliasresolver/impl/src/main/res/values-ca/translations.xml b/features/roomaliasresolver/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..ea90cabc34 --- /dev/null +++ b/features/roomaliasresolver/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,5 @@ + + + "No s\'ha pogut mostrar la vista prèvia de la sala" + "No s\'ha pogut obtenir l\'àlies de sala." + diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt index 4b37f993f9..ae33517732 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomaliasresolver.impl 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.ui.strings.CommonStrings @@ -22,48 +24,43 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomAliasHelperViewTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomAliasHelperViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setRoomAliasResolverView( + setRoomAliasResolverView( aRoomAliasResolverState( eventSink = eventsRecorder, ), onBackClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on Retry emits the expected Event`() { + fun `clicking on Retry emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomAliasResolverView( + setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Failure(Exception("Error")), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_retry) + clickOn(CommonStrings.action_retry) eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry) } @Test - fun `success state invokes the expected Callback`() { + fun `success state invokes the expected Callback`() = runAndroidComposeUiTest { val result = aResolvedRoomAlias() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(result) { - rule.setRoomAliasResolverView( + setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Success(result), eventSink = eventsRecorder, @@ -74,7 +71,7 @@ class RoomAliasHelperViewTest { } } -private fun AndroidComposeTestRule.setRoomAliasResolverView( +private fun AndroidComposeUiTest.setRoomAliasResolverView( state: RoomAliasResolverState, onBackClick: () -> Unit = EnsureNeverCalled(), onAliasResolved: (ResolvedRoomAlias) -> Unit = EnsureNeverCalledWithParam(), 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 3d863321e3..15e320c8f8 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 @@ -23,7 +23,6 @@ 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 import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState 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 4c6fcf8e59..0612adbea1 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 @@ -89,10 +89,10 @@ class RoomCallStatePresenterTest { } @Test - fun `present - initial state - when is direct room`() = runTest { + fun `present - initial state - when is DM room`() = runTest { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( - initialRoomInfo = aRoomInfo(isDirect = true), + initialRoomInfo = aRoomInfo(isDm = true), roomPermissions = roomPermissions(true), ) ) 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 07e10c65ef..3474711b55 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 @@ -38,9 +38,10 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { data class Params(val initialElement: InitialTarget) : NodeInputs interface Callback : Plugin { + fun onDone() fun navigateToGlobalNotificationSettings() fun navigateToDeveloperSettings() - fun navigateToRoom(roomId: RoomId, serverNames: List) + fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean = false) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) } diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 2765a95a1e..77e2d0c229 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.push.api) implementation(projects.libraries.testtags) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) @@ -69,6 +70,7 @@ dependencies { testImplementation(projects.libraries.mediaviewer.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.features.call.test) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt index de801e8aae..a82766175d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsEvent.kt @@ -14,4 +14,6 @@ sealed interface RoomDetailsEvent { data object UnmuteNotification : RoomDetailsEvent data class CopyToClipboard(val text: String) : RoomDetailsEvent data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent + data object MarkAsRead : RoomDetailsEvent + data object MarkAsUnread : RoomDetailsEvent } 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 c3ae902ba9..7574863962 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 @@ -25,7 +25,7 @@ import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.annotations.ContributesNode import io.element.android.appconfig.LearnMoreConfig -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint @@ -176,6 +176,10 @@ class RoomDetailsFlowNode( return when (navTarget) { NavTarget.RoomDetails -> { val roomDetailsCallback = object : RoomDetailsNode.Callback { + override fun navigateBack() { + callback.onDone() + } + override fun navigateToRoomMemberList() { backstack.push(NavTarget.RoomMemberList) } @@ -225,13 +229,13 @@ class RoomDetailsFlowNode( } override fun navigateToRoomCall(callIntent: CallIntent) { - val inputs = CallType.RoomCall( + val callData = CallData( sessionId = room.sessionId, roomId = room.roomId, isAudioCall = callIntent == CallIntent.AUDIO ) analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) - elementCallEntryPoint.startCall(inputs) + elementCallEntryPoint.startCall(callData) } override fun navigateToReportRoom() { @@ -254,6 +258,10 @@ class RoomDetailsFlowNode( override fun navigateToInviteMembers() { backstack.push(NavTarget.InviteMembers) } + + override fun navigateToAvatarPreview(username: String, avatarUrl: String) { + overlay.show(NavTarget.AvatarPreview(username, avatarUrl)) + } } createNode(buildContext, listOf(roomMemberListCallback)) } @@ -263,7 +271,20 @@ class RoomDetailsFlowNode( } NavTarget.InviteMembers -> { - createNode(buildContext) + val callback = object : RoomInviteMembersNode.Callback { + override fun openCreatedRoom(roomId: RoomId) { + navigateUp() + room.roomCoroutineScope.launch { + callback.navigateToRoom( + roomId = roomId, + serverNames = emptyList(), + // Remove the invite screen from the backstack to avoid navigating back to it after the new room has been created + clearBackStack = true, + ) + } + } + } + createNode(buildContext, plugins = listOf(callback)) } is NavTarget.RoomNotificationSettings -> { @@ -288,7 +309,7 @@ class RoomDetailsFlowNode( override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) { elementCallEntryPoint.startCall( - CallType.RoomCall( + CallData( roomId = dmRoomId, sessionId = room.sessionId, isAudioCall = callIntent == CallIntent.AUDIO diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNavigator.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNavigator.kt new file mode 100644 index 0000000000..9e2b77a260 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNavigator.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.roomdetails.impl + +interface RoomDetailsNavigator { + fun onDone() +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index e9aad5b1d2..7f22c7e111 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -42,12 +42,13 @@ import io.element.android.libraries.androidutils.R as AndroidUtilsR class RoomDetailsNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: RoomDetailsPresenter, + presenterFactory: RoomDetailsPresenter.Factory, private val room: BaseRoom, private val analyticsService: AnalyticsService, private val leaveRoomRenderer: LeaveRoomRenderer, -) : Node(buildContext, plugins = plugins) { +) : Node(buildContext, plugins = plugins), RoomDetailsNavigator { interface Callback : Plugin { + fun navigateBack() fun navigateToRoomMemberList() fun navigateToInviteMembers() fun navigateToRoomDetailsEdit() @@ -65,6 +66,7 @@ class RoomDetailsNode( fun navigateToSelectNewOwnersWhenLeaving() } + private val presenter = presenterFactory.create(this) private val callback: Callback = callback() init { @@ -144,4 +146,8 @@ class RoomDetailsNode( } ) } + + override fun onDone() { + callback.navigateBack() + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 5fd44076e6..9a602d06c0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -17,7 +17,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedFactory +import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.knockrequests.api.KnockRequestPermissions import io.element.android.features.knockrequests.api.knockRequestPermissions @@ -35,36 +37,36 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient +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.notificationsettings.NotificationSettingsService 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.isDm import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.canEditRolesAndPermissions import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.roomNotificationSettings -import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.ui.room.getDirectRoomMember import io.element.android.libraries.matrix.ui.room.roomMemberIdentityStateChange import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.push.api.notifications.NotificationCleaner import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -@Inject +@AssistedInject class RoomDetailsPresenter( + @Assisted private val navigator: RoomDetailsNavigator, private val client: MatrixClient, private val room: JoinedRoom, - private val featureFlagService: FeatureFlagService, private val notificationSettingsService: NotificationSettingsService, private val roomMembersDetailsPresenterFactory: RoomMemberDetailsPresenter.Factory, private val leaveRoomPresenter: Presenter, @@ -73,7 +75,16 @@ class RoomDetailsPresenter( private val analyticsService: AnalyticsService, private val clipboardHelper: ClipboardHelper, private val appPreferencesStore: AppPreferencesStore, + private val sessionPreferencesStore: SessionPreferencesStore, + private val notificationCleaner: NotificationCleaner, ) : Presenter { + @AssistedFactory + interface Factory { + fun create( + navigator: RoomDetailsNavigator, + ): RoomDetailsPresenter + } + @Composable override fun present(): RoomDetailsState { val scope = rememberCoroutineScope() @@ -85,6 +96,14 @@ class RoomDetailsPresenter( val roomTopic by remember { derivedStateOf { roomInfo.topic } } val isFavorite by remember { derivedStateOf { roomInfo.isFavorite } } val joinRule by remember { derivedStateOf { roomInfo.joinRule } } + val hasNewContent by remember { + derivedStateOf { + roomInfo.numUnreadMessages > 0 || + roomInfo.numUnreadMentions > 0 || + roomInfo.numUnreadNotifications > 0 || + roomInfo.isMarkedUnread + } + } val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } } @@ -99,9 +118,8 @@ class RoomDetailsPresenter( val canonicalAlias by remember { derivedStateOf { roomInfo.canonicalAlias } } val isEncrypted by remember { derivedStateOf { roomInfo.isEncrypted == true } } val dmMember by room.getDirectRoomMember(membersState) - val currentMember by room.getCurrentRoomMember(membersState) - val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember) - val roomType = getRoomType(dmMember, currentMember) + val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember?.userId) + val roomType = getRoomType(dmMember) val roomCallState = roomCallStatePresenter.present() val joinedMemberCount by remember { derivedStateOf { roomInfo.joinedMembersCount } } @@ -114,14 +132,11 @@ class RoomDetailsPresenter( } } - val isKnockRequestsEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) - }.collectAsState(false) val knockRequestsCount by produceState(null) { room.knockRequestsFlow.collect { value = it.size } } val canShowKnockRequests by remember { - derivedStateOf { isKnockRequestsEnabled && permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock } + derivedStateOf { permissions.knockRequestsPermissions.hasAny && joinRule == JoinRule.Knock } } val canShowSecurityAndPrivacy by remember { derivedStateOf { !isDm && permissions.securityAndPrivacyPermissions.hasAny(isSpace = false, joinRule = joinRule) } @@ -147,7 +162,7 @@ class RoomDetailsPresenter( } RoomDetailsEvent.UnmuteNotification -> { scope.launch(dispatchers.io) { - notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isOneToOne) + notificationSettingsService.unmuteRoom(room.roomId, isEncrypted, room.isDm()) } } is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite) @@ -155,10 +170,12 @@ class RoomDetailsPresenter( clipboardHelper.copyPlainText(event.text) snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) } + is RoomDetailsEvent.MarkAsRead -> scope.markAsRead() + is RoomDetailsEvent.MarkAsUnread -> scope.markAsUnread() } } - val roomMemberDetailsState = roomMemberDetailsPresenter?.present() + val dmOtherMemberDetailsState = roomMemberDetailsPresenter?.present() val hasMemberVerificationViolations by produceState(false) { room.roomMemberIdentityStateChange(waitForEncryption = true) @@ -180,45 +197,40 @@ class RoomDetailsPresenter( canEdit = roomType == RoomDetailsType.Room && permissions.editDetailsPermissions.hasAny, roomCallState = roomCallState, roomType = roomType, - roomMemberDetailsState = roomMemberDetailsState, + dmOtherMemberDetailsState = dmOtherMemberDetailsState, leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, displayRolesAndPermissionsSettings = !isDm && permissions.canEditRolesAndPermissions, isPublic = joinRule == JoinRule.Public, - heroes = roomInfo.heroes.toImmutableList(), + heroes = roomInfo.heroes, pinnedMessagesCount = pinnedMessagesCount, snackbarMessage = snackbarMessage, canShowKnockRequests = canShowKnockRequests, knockRequestsCount = knockRequestsCount, canShowSecurityAndPrivacy = canShowSecurityAndPrivacy, hasMemberVerificationViolations = hasMemberVerificationViolations, - canReportRoom = canReportRoom, + canReportRoom = !isDm && canReportRoom, isTombstoned = roomInfo.successorRoom != null, showDebugInfo = isDeveloperModeEnabled, roomVersion = roomInfo.roomVersion, roomHistoryVisibility = roomInfo.historyVisibility, + hasNewContent = hasNewContent, eventSink = ::handleEvent, ) } @Composable - private fun roomMemberDetailsPresenter(dmMemberState: RoomMember?) = remember(dmMemberState) { - dmMemberState?.let { roomMember -> - roomMembersDetailsPresenterFactory.create(roomMember.userId) + private fun roomMemberDetailsPresenter(userId: UserId?) = remember(userId) { + userId?.let { userId -> + roomMembersDetailsPresenterFactory.create(userId) } } @Composable - private fun getRoomType( - dmMember: RoomMember?, - currentMember: RoomMember?, - ): RoomDetailsType = remember(dmMember, currentMember) { - if (dmMember != null && currentMember != null) { - RoomDetailsType.Dm( - me = currentMember, - otherMember = dmMember, - ) + private fun getRoomType(dmMember: RoomMember?): RoomDetailsType = remember(dmMember) { + if (dmMember != null) { + RoomDetailsType.Dm(otherMember = dmMember) } else { RoomDetailsType.Room } @@ -257,4 +269,26 @@ class RoomDetailsPresenter( analyticsService.captureInteraction(Interaction.Name.MobileRoomFavouriteToggle) } } + + private fun CoroutineScope.markAsRead() = launch { + notificationCleaner.clearMessagesForRoom(client.sessionId, room.roomId) + room.setUnreadFlag(isUnread = false) + val receiptType = if (sessionPreferencesStore.isSendPublicReadReceiptsEnabled().first()) { + ReceiptType.READ + } else { + ReceiptType.READ_PRIVATE + } + room.markAsRead(receiptType) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle) + } + } + + private fun CoroutineScope.markAsUnread() = launch { + room.setUnreadFlag(isUnread = true) + .onSuccess { + analyticsService.captureInteraction(name = Interaction.Name.MobileRoomListRoomContextMenuUnreadToggle) + navigator.onDone() + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt index e74f71322d..4464058701 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt @@ -31,7 +31,7 @@ data class RoomDetailsState( val memberCount: Long, val isEncrypted: Boolean, val roomType: RoomDetailsType, - val roomMemberDetailsState: UserProfileState?, + val dmOtherMemberDetailsState: UserProfileState?, val canEdit: Boolean, val canInvite: Boolean, val roomCallState: RoomCallState, @@ -52,6 +52,7 @@ data class RoomDetailsState( val showDebugInfo: Boolean, val roomVersion: String?, val roomHistoryVisibility: RoomHistoryVisibility, + val hasNewContent: Boolean, val eventSink: (RoomDetailsEvent) -> Unit ) { val roomBadges = buildList { @@ -77,10 +78,7 @@ data class RoomDetailsState( @Immutable sealed interface RoomDetailsType { data object Room : RoomDetailsType - data class Dm( - val me: RoomMember, - val otherMember: RoomMember, - ) : RoomDetailsType + data class Dm(val otherMember: RoomMember) : RoomDetailsType } @Immutable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 7f3701a770..935b7293ae 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -13,7 +13,6 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState -import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.aUserProfileState @@ -35,7 +34,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider override val values: Sequence get() = sequenceOf( aRoomDetailsState(displayAdminSettings = true), - aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true), + aRoomDetailsState(roomTopic = RoomTopicState.Hidden, showDebugInfo = true, hasNewContent = true), aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic), aRoomDetailsState(isEncrypted = false), aRoomDetailsState(roomAlias = null), @@ -75,6 +74,7 @@ fun aDmRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, + isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -84,7 +84,8 @@ fun aDmRoomMember( powerLevel = powerLevel, isIgnored = isIgnored, role = role, - membershipChangeReason = membershipChangeReason + membershipChangeReason = membershipChangeReason, + isServiceMember = isServiceMember, ) fun aRoomDetailsState( @@ -105,7 +106,7 @@ fun aRoomDetailsState( canEdit: Boolean = false, roomCallState: RoomCallState = aStandByCallState(), roomType: RoomDetailsType = RoomDetailsType.Room, - roomMemberDetailsState: UserProfileState? = null, + dmOtherMemberDetailsState: UserProfileState? = null, leaveRoomState: LeaveRoomState = aLeaveRoomState(), roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(), isFavorite: Boolean = false, @@ -122,6 +123,7 @@ fun aRoomDetailsState( isTombstoned: Boolean = false, showDebugInfo: Boolean = false, roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared, + hasNewContent: Boolean = false, eventSink: (RoomDetailsEvent) -> Unit = {}, ) = RoomDetailsState( roomId = roomId, @@ -135,7 +137,7 @@ fun aRoomDetailsState( canEdit = canEdit, roomCallState = roomCallState, roomType = roomType, - roomMemberDetailsState = roomMemberDetailsState, + dmOtherMemberDetailsState = dmOtherMemberDetailsState, leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettings, isFavorite = isFavorite, @@ -153,6 +155,7 @@ fun aRoomDetailsState( showDebugInfo = showDebugInfo, roomVersion = "12", roomHistoryVisibility = roomHistoryVisibility, + hasNewContent = hasNewContent, eventSink = eventSink, ) @@ -179,11 +182,9 @@ fun aDmRoomDetailsState( roomName = roomName, isPublic = false, isEncrypted = isEncrypted, - roomType = RoomDetailsType.Dm( - me = aRoomMember(), - otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored), - ), - roomMemberDetailsState = aUserProfileState( + canInvite = true, + roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), + dmOtherMemberDetailsState = aUserProfileState( isBlocked = AsyncData.Success(isDmMemberIgnored), verificationState = dmRoomMemberVerificationState, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index c48716db11..591ff7ff1e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -8,6 +8,7 @@ package io.element.android.features.roomdetails.impl +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -31,6 +33,7 @@ import androidx.compose.runtime.remember 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.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -40,6 +43,7 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomcall.api.hasPermissionToJoin +import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection @@ -52,7 +56,6 @@ 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.DmAvatars import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.MainActionButton import io.element.android.libraries.designsystem.components.list.ListItemContent @@ -62,7 +65,7 @@ import io.element.android.libraries.designsystem.modifiers.niceClickable import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight +import io.element.android.libraries.designsystem.preview.PreviewWithExtraLargeHeight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem @@ -91,8 +94,12 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.compose.LocalAnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +/** + * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=21-120385 + */ @Composable fun RoomDetailsView( state: RoomDetailsState, @@ -154,9 +161,9 @@ fun RoomDetailsView( } is RoomDetailsType.Dm -> { DmHeaderSection( - me = state.roomType.me, otherMember = state.roomType.otherMember, roomName = state.roomName, + isTombstoned = state.isTombstoned, openAvatarPreview = { name, avatarUrl -> openAvatarPreview(name, avatarUrl) }, @@ -187,58 +194,50 @@ fun RoomDetailsView( } PreferenceCategory { - if (state.roomNotificationSettings != null) { - NotificationItem( - isDefaultMode = state.roomNotificationSettings.isDefault, - openRoomNotificationSettings = openRoomNotificationSettings + if (state.hasNewContent) { + ListItem( + headlineContent = { + Text( + text = stringResource(id = R.string.screen_roomlist_mark_as_read), + style = MaterialTheme.typography.bodyLarge, + ) + }, + onClick = { + state.eventSink(RoomDetailsEvent.MarkAsRead) + }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.MarkAsRead()) + ), + trailingContent = ListItemContent.Custom { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(ElementTheme.colors.iconAccentPrimary) + ) + }, ) - } - - FavoriteItem( - isFavorite = state.isFavorite, - onFavoriteChanges = { - state.eventSink(RoomDetailsEvent.SetFavorite(it)) - } - ) - - if (state.canShowSecurityAndPrivacy) { - SecurityAndPrivacyItem( - onClick = onSecurityAndPrivacyClick - ) - } - - state.roomMemberDetailsState?.let { dmMemberDetails -> - ProfileItem( - verificationState = dmMemberDetails.verificationState, - onClick = { onProfileClick(dmMemberDetails.userId) } + } else { + ListItem( + headlineContent = { + Text( + text = stringResource(id = R.string.screen_roomlist_mark_as_unread), + ) + }, + onClick = { + state.eventSink(RoomDetailsEvent.MarkAsUnread) + }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(CompoundIcons.MarkAsUnread()) + ), ) } } - - if (state.roomType is RoomDetailsType.Room) { - PreferenceCategory { - MembersItem( - memberCount = state.memberCount, - hasVerificationViolations = state.hasMemberVerificationViolations, - openRoomMemberList = openRoomMemberList, - ) - if (state.canShowKnockRequests) { - KnockRequestsItem( - knockRequestsCount = state.knockRequestsCount, - onKnockRequestsClick = onKnockRequestsClick - ) - } - if (state.displayRolesAndPermissionsSettings) { - ListItem( - headlineContent = { Text(stringResource(R.string.screen_room_details_roles_and_permissions)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), - onClick = openAdminSettings, - ) - } - } - } - + // Room content PreferenceCategory { + MediaGalleryItem( + onClick = openMediaGallery + ) PinnedMessagesItem( pinnedMessagesCount = state.pinnedMessagesCount, onPinnedMessagesClick = onPinnedMessagesClick @@ -246,23 +245,87 @@ fun RoomDetailsView( PollsItem( openPollHistory = openPollHistory ) - MediaGalleryItem( - onClick = openMediaGallery + } + when (state.roomType) { + is RoomDetailsType.Room -> { + PreferenceCategory { + MembersItem( + memberCount = state.memberCount, + hasVerificationViolations = state.hasMemberVerificationViolations, + openRoomMemberList = openRoomMemberList, + ) + if (state.canShowKnockRequests) { + KnockRequestsItem( + knockRequestsCount = state.knockRequestsCount, + onKnockRequestsClick = onKnockRequestsClick + ) + } + if (state.displayRolesAndPermissionsSettings) { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_roles_and_permissions)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), + onClick = openAdminSettings, + ) + } + } + } + is RoomDetailsType.Dm -> { + if (state.canInvite) { + // Note: for rooms the invite action is a Main action + PreferenceCategory { + InviteItem(onClick = invitePeople) + } + } + state.dmOtherMemberDetailsState?.let { dmMemberDetails -> + PreferenceCategory { + ProfileItem( + verificationState = dmMemberDetails.verificationState, + onClick = { onProfileClick(dmMemberDetails.userId) } + ) + } + } + } + } + PreferenceCategory { + if (state.roomNotificationSettings != null) { + NotificationItem( + isDefaultMode = state.roomNotificationSettings.isDefault, + openRoomNotificationSettings = openRoomNotificationSettings + ) + } + FavoriteItem( + isFavorite = state.isFavorite, + onFavoriteChanges = { + state.eventSink(RoomDetailsEvent.SetFavorite(it)) + } ) + if (state.canShowSecurityAndPrivacy && state.roomType is RoomDetailsType.Room) { + SecurityAndPrivacyItem( + onClick = onSecurityAndPrivacyClick + ) + } } - - if (state.roomType is RoomDetailsType.Dm && state.roomMemberDetailsState != null) { - val roomMemberState = state.roomMemberDetailsState - BlockUserSection(roomMemberState) - BlockUserDialogs(roomMemberState) + if (state.isEncrypted) { + PreferenceCategory( + title = stringResource(R.string.screen_room_details_security_title) + ) { + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), + headlineContent = { + Text(stringResource(id = R.string.screen_room_details_encryption_enabled_title)) + }, + supportingContent = { + Text(stringResource(id = R.string.screen_room_details_encryption_enabled_subtitle)) + }, + ) + } } - OtherActionsSection( + dmOtherMemberDetailsState = state.dmOtherMemberDetailsState, canReportRoom = state.canReportRoom, onReportRoomClick = onReportRoomClick, onLeaveRoomClick = { state.eventSink(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) } ) - if (state.showDebugInfo) { DebugInfoSection( roomId = state.roomId, @@ -372,14 +435,14 @@ private fun MainActionsSection( onClick = { onCall(CallIntent.VIDEO) }, ) } + if (state.canInvite && state.roomType !is RoomDetailsType.Dm) { + MainActionButton( + title = stringResource(CommonStrings.action_invite), + imageVector = CompoundIcons.UserAdd(), + onClick = onInvitePeople, + ) + } if (state.roomType is RoomDetailsType.Room) { - if (state.canInvite) { - MainActionButton( - title = stringResource(CommonStrings.action_invite), - imageVector = CompoundIcons.UserAdd(), - onClick = onInvitePeople, - ) - } // Share CTA should be hidden for DMs MainActionButton( title = stringResource(CommonStrings.action_share), @@ -417,6 +480,7 @@ private fun RoomHeaderSection( ), contentDescription = stringResource(CommonStrings.a11y_room_avatar), modifier = Modifier + .clip(CircleShape) .clickable( enabled = avatarUrl != null, onClickLabel = stringResource(CommonStrings.action_view), @@ -435,9 +499,9 @@ private fun RoomHeaderSection( @Composable private fun DmHeaderSection( - me: RoomMember, otherMember: RoomMember, roomName: String, + isTombstoned: Boolean, openAvatarPreview: (name: String, url: String) -> Unit, onSubtitleClick: (String) -> Unit, modifier: Modifier = Modifier @@ -448,11 +512,24 @@ private fun DmHeaderSection( .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - DmAvatars( - userAvatarData = me.getAvatarData(size = AvatarSize.DmCluster), - otherUserAvatarData = otherMember.getAvatarData(size = AvatarSize.DmCluster), - openAvatarPreview = { url -> openAvatarPreview(me.getBestName(), url) }, - openOtherAvatarPreview = { url -> openAvatarPreview(roomName, url) }, + Avatar( + avatarData = AvatarData(otherMember.userId.value, roomName, otherMember.avatarUrl, AvatarSize.RoomDetailsHeader), + avatarType = AvatarType.Room( + heroes = persistentListOf( + otherMember.getAvatarData(size = AvatarSize.RoomDetailsHeader) + ), + isTombstoned = isTombstoned, + ), + contentDescription = stringResource(CommonStrings.a11y_room_avatar), + modifier = Modifier + .clip(CircleShape) + .clickable( + enabled = otherMember.avatarUrl != null, + onClickLabel = stringResource(CommonStrings.action_view), + ) { + openAvatarPreview(otherMember.getBestName(), otherMember.avatarUrl!!) + } + .testTag(TestTags.roomDetailAvatar) ) TitleAndSubtitle( title = roomName, @@ -677,6 +754,17 @@ private fun MembersItem( ) } +@Composable +private fun InviteItem( + onClick: () -> Unit, +) { + ListItem( + headlineContent = { Text(stringResource(R.string.screen_room_details_invite_title)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserAdd())), + onClick = onClick, + ) +} + @Composable private fun PinnedMessagesItem( pinnedMessagesCount: Int?, @@ -728,8 +816,13 @@ private fun OtherActionsSection( canReportRoom: Boolean, onReportRoomClick: () -> Unit, onLeaveRoomClick: () -> Unit, + dmOtherMemberDetailsState: UserProfileState?, ) { - PreferenceCategory(showTopDivider = true) { + PreferenceCategory { + if (dmOtherMemberDetailsState != null) { + BlockUserSection(dmOtherMemberDetailsState) + BlockUserDialogs(dmOtherMemberDetailsState) + } if (canReportRoom) { ListItem( headlineContent = { @@ -757,7 +850,7 @@ private fun DebugInfoSection( roomVersion: String?, ) { val context = LocalContext.current - PreferenceCategory(showTopDivider = true) { + PreferenceCategory { val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard) ListItem( headlineContent = { @@ -795,17 +888,17 @@ private fun DebugInfoSection( } } -@PreviewWithLargeHeight +@PreviewWithExtraLargeHeight @Composable internal fun RoomDetailsPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = ElementPreviewLight { ContentToPreview(state) } -@PreviewWithLargeHeight +@PreviewWithExtraLargeHeight @Composable internal fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = ElementPreviewDark { ContentToPreview(state) } -@PreviewWithLargeHeight +@PreviewWithExtraLargeHeight @Composable internal fun RoomDetailsA11yPreview() = ElementPreview { ContentToPreview( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt index ea0ed1bb72..3919817313 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -11,6 +11,7 @@ package io.element.android.features.roomdetails.impl.invite import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -19,10 +20,16 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode +import io.element.android.features.invitepeople.api.InvitePeopleEvents import io.element.android.features.invitepeople.api.InvitePeoplePresenter import io.element.android.features.invitepeople.api.InvitePeopleRenderer +import io.element.android.libraries.architecture.callback +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) @@ -35,6 +42,10 @@ class RoomInviteMembersNode( room: JoinedRoom, invitePeoplePresenterFactory: InvitePeoplePresenter.Factory, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openCreatedRoom(roomId: RoomId) + } + init { lifecycle.subscribe( onResume = { @@ -48,6 +59,8 @@ class RoomInviteMembersNode( roomId = room.roomId, ) + private val callback = plugins.callback() + @Composable override fun View(modifier: Modifier) { val state = invitePeoplePresenter.present() @@ -59,6 +72,19 @@ class RoomInviteMembersNode( } } + AsyncActionView( + async = state.createRoomFromDmAction, + onSuccess = { roomId -> + callback.openCreatedRoom(roomId) + }, + progressDialog = { + ProgressDialog(text = stringResource(CommonStrings.common_creating_room)) + }, + onErrorDismiss = { + state.eventSink(InvitePeopleEvents.ClearError) + } + ) + RoomInviteMembersView( state = state, modifier = modifier, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 750b111fc3..37f107e784 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) @@ -41,6 +42,7 @@ class RoomMemberListNode( interface Callback : Plugin { fun navigateToRoomMemberDetails(roomMemberId: UserId) fun navigateToInviteMembers() + fun navigateToAvatarPreview(username: String, avatarUrl: String) } private val callback: Callback = callback() @@ -82,13 +84,18 @@ class RoomMemberListNode( else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) } }, + onAvatarClick = { user -> + user.avatarUrl?.let { url -> + callback.navigateToAvatarPreview(user.getBestName(), url) + } + }, modifier = Modifier, ) } } interface RoomMemberListNavigator { - fun exitRoomMemberList() {} - fun openRoomMemberDetails(roomMemberId: UserId) {} - fun openInviteMembers() {} + fun exitRoomMemberList() + fun openRoomMemberDetails(roomMemberId: UserId) + fun openInviteMembers() } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index bc077feb6a..23c6292294 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -15,6 +15,15 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.map +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID +import io.element.android.libraries.designsystem.preview.USER_NAME_EVE +import io.element.android.libraries.designsystem.preview.USER_NAME_MALLORY +import io.element.android.libraries.designsystem.preview.USER_NAME_SUSIE +import io.element.android.libraries.designsystem.preview.USER_NAME_VICTOR +import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER 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.RoomMember @@ -119,6 +128,7 @@ fun aRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, + isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -129,6 +139,7 @@ fun aRoomMember( isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, + isServiceMember = isServiceMember, ) fun aRoomMemberList() = persistentListOf( @@ -143,21 +154,21 @@ fun aRoomMemberList() = persistentListOf( aBannedMallory(), ) -fun anEve(): RoomMember = aRoomMember(UserId("@eve:server.org"), "Eve") +fun anEve(): RoomMember = aRoomMember(UserId("@eve:server.org"), USER_NAME_EVE) -fun aDavid(): RoomMember = aRoomMember(UserId("@david:server.org"), "David") +fun aDavid(): RoomMember = aRoomMember(UserId("@david:server.org"), USER_NAME_DAVID) -fun aCarol(): RoomMember = aRoomMember(UserId("@carol:server.org"), "Carol") +fun aCarol(): RoomMember = aRoomMember(UserId("@carol:server.org"), USER_NAME_CAROL) -fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin) -fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator) +fun anAlice() = aRoomMember(UserId("@alice:server.org"), USER_NAME_ALICE, role = RoomMember.Role.Admin) +fun aBob() = aRoomMember(UserId("@bob:server.org"), USER_NAME_BOB, role = RoomMember.Role.Moderator) -fun anInvitedVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE) +fun anInvitedVictor() = aRoomMember(UserId("@victor:server.org"), USER_NAME_VICTOR, membership = RoomMembershipState.INVITE) -fun anInvitedWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE) +fun anInvitedWalter() = aRoomMember(UserId("@walter:server.org"), USER_NAME_WALTER, membership = RoomMembershipState.INVITE) -fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), "Susie", membership = RoomMembershipState.BAN) +fun aBannedSusie(): RoomMember = aRoomMember(UserId("@susie:server.org"), USER_NAME_SUSIE, membership = RoomMembershipState.BAN) -fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), "Mallory", membership = RoomMembershipState.BAN) +fun aBannedMallory(): RoomMember = aRoomMember(UserId("@mallory:server.org"), USER_NAME_MALLORY, membership = RoomMembershipState.BAN) private fun RoomMember.withIdentity(identityState: IdentityState? = null) = RoomMemberWithIdentityState(this, identityState) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 146d2f7952..0e6f5b6c88 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -50,6 +50,7 @@ import io.element.android.libraries.designsystem.theme.components.SegmentedButto import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar +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.RoomMember import io.element.android.libraries.matrix.api.room.getBestName @@ -355,6 +356,10 @@ private fun RoomMemberListTopBar( internal fun RoomMemberListViewPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = ElementPreview { RoomMemberListView( state = state, - navigator = object : RoomMemberListNavigator {}, + navigator = object : RoomMemberListNavigator { + override fun exitRoomMemberList() {} + override fun openRoomMemberDetails(roomMemberId: UserId) {} + override fun openInviteMembers() {} + }, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index c7930b5895..08dbf2019d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -160,7 +160,7 @@ class RoomNotificationSettingsPresenter( suspend { val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() pendingModeState.value = null - notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isOneToOne).getOrThrow() + notificationSettingsService.getRoomNotificationSettings(room.roomId, isEncrypted, room.isDm()).getOrThrow() }.runCatchingUpdatingState(roomNotificationSettings) } @@ -170,7 +170,7 @@ class RoomNotificationSettingsPresenter( val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( isEncrypted, - room.isOneToOne + room.isDm() ).getOrThrow() } diff --git a/features/roomdetails/impl/src/main/res/values-be/translations.xml b/features/roomdetails/impl/src/main/res/values-be/translations.xml index 9b89f2cef0..2b0b59e359 100644 --- a/features/roomdetails/impl/src/main/res/values-be/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml @@ -1,14 +1,16 @@ + "Рэдагаваць адрас" "Пры абнаўленні налад апавяшчэнняў адбылася памылка." "Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях." "Апытанні" - "Толькі адміністратары" + "Адміністратар" "Заблакіраваць людзей" "Выдаліць паведамленні" - "Запрашайце людзей і прымайце запыты на далучэнне" + "Удзельнік" + "Запрасіць людзей" "Паведамленні і змест" - "Адміністратары і мадэратары" + "Мадэратар" "Выдаляйце людзей і адхіляйце запыты на далучэнне" "Змяніць аватар пакоя" "Рэдагаваць пакой" @@ -43,6 +45,7 @@ "Не атрымалася адключыць гук у гэтым пакоі, паўтарыце спробу." "Не ўдалося ўключыць гук у гэтым пакоі. Паўтарыце спробу." "Запрасіць карыстальнікаў" + "Запрасіць" "Пакінуць размову" "Пакінуць пакой" "Уласныя" @@ -51,8 +54,9 @@ "Замацаваныя паведамленні" "Профіль" "Ролі і дазволы" + "Імя" "Бяспека" - "Падзяліцца пакоем" + "Абагуліць пакой" "Інфармацыя аб пакоі" "Тэма" "Ідзе абнаўленне пакоя…" @@ -68,8 +72,8 @@ "Яны змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." "Заблакіраваныя" "Удзельнікі" - "Толькі адміністратары" - "Адміністратары і мадэратары" + "Адміністратар" + "Мадэратар" "Удзельнікі пакоя" "Разблакіроўка %1$s" "Дазволіць уласную наладу" @@ -99,4 +103,7 @@ "Ролі" "Дэталі пакоя" "Ролі і дазволы" + "Пазначыць як прачытанае" + "Пазначыць як непрачытанае" + "Адрас" diff --git a/features/roomdetails/impl/src/main/res/values-bg/translations.xml b/features/roomdetails/impl/src/main/res/values-bg/translations.xml index 55bce6cd12..7369d78f1d 100644 --- a/features/roomdetails/impl/src/main/res/values-bg/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml @@ -33,6 +33,7 @@ "Неуспешно заглушаване на тази стая, моля, опитайте отново." "Неуспешно раззаглушаване на тази стая, моля, опитайте отново." "Поканване на хора" + "Поканване" "Напускане на разговора" "Напускане на стаята" "Медия и файлове" @@ -78,6 +79,8 @@ "Роли" "Подробности за стаята" "Роли и разрешения" + "Отбелязване като прочетено" + "Отбелязване като непрочетено" "Добавяне на адрес" "Да, включване на шифроването" "Да се включи ли шифроването?" diff --git a/features/roomdetails/impl/src/main/res/values-ca/translations.xml b/features/roomdetails/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..8b9f8d937e --- /dev/null +++ b/features/roomdetails/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,141 @@ + + + "És necessària una adreça perquè sigui visible al directori públic." + "Edita adreça" + "S\'ha produït un error en actualitzar la configuració de notificacions." + "El servidor no admet aquesta opció en sales xifrades, és possible que no rebis notificacions en algunes sales." + "Votacions" + "Només administradors" + "Bandejar usuaris" + "Eliminar missatges" + "Membre" + "Convidar persones i acceptar sol·licituds d\'unió" + "Gestiona membres" + "Missatges i contingut" + "Administradors i moderadors" + "Eliminar persones i rebutjar sol·licituds d\'unió" + "Canvia la foto de la sala" + "Edita detalls" + "Canvia el nom de la sala" + "Canvia el tema de la sala" + "Enviar missatges" + "Edita administradors" + "No podràs desfer aquesta acció. Estàs concedint a l\'usuari el mateix nivell de poder que tu." + "Afegir com a administrador?" + "Descendeix" + "No podràs desfer aquest canvi ja que t\'estàs descendint de rang, si ets l\'últim usuari de la sala amb privilegis, no podràs recuperar-los." + "Vols descendir de categoria?" + "%1$s (pendent)" + "(pendent)" + "Els administradors tenen automàticament privilegis de moderador" + "Edita moderadors" + "Administradors" + "Moderadors" + "Membres" + "Hi ha canvis sense desar." + "Desar canvis?" + "Afegeix tema" + "Xifrada" + "No xifrada" + "Sala pública" + "Edita detalls" + "Un error desconegut ha impedit l\'intercanvi d\'informació." + "No s\'ha pogut actualitzar la sala" + "Els missatges estan protegits amb cadenats. Només tu i els destinataris teniu les úniques claus per a desbloquejar-los." + "Xifrat de missatges activat" + "S\'ha produït un error en carregar la configuració de notificacions." + "No s\'ha pogut silenciar la sala. Torna-ho a intentar." + "No s\'ha pogut deixar de silenciar la sala. Torna-ho a provar." + "Convida persones" + "Convida" + "Surt del xat" + "Surt de la sala" + "Multimèdia i documents" + "Personalitzat" + "Predeterminat" + "Notificacions" + "Missatges fixats" + "Perfil" + "Sol·licituds d\'unió" + "Rols i permisos" + "Seguretat i privadesa" + "Seguretat" + "Comparteix sala" + "Informació de sala" + "Tema" + "Actualitzant sala…" + "No hi ha usuaris bandejats en aquesta sala." + + "%1$d persona" + "%1$d persones" + + "Bandeja de la sala" + "Només elimina\'l" + "Desbandeja" + "Podran tornar a unir-se a aquesta sala si se\'ls convida." + "Readmet usuari" + "Bandejats" + "Membres" + "Només administradors" + "Administradors i moderadors" + "Membres de la sala" + "Readmetent %1$s" + "Permet la configuració personalitzada" + "Si ho actives, se substituirà la configuració predeterminada" + "Notifica\'m en aquest xat" + "Ho pots canviar al la %1$s." + "configuració global" + "Configuració predeterminada" + "Elimina la configuració personalitzada" + "S\'ha produït un error en carregar la configuració de notificacions." + "No s\'ha pogut restaurar el mode predeterminat. Torna-ho a provar." + "No s\'ha pogut configurar el mode. Torna-ho a provar." + "El servidor no admet aquesta opció en sales xifrades, no rebras notificacions en aquesta sala." + "Tots els missatges" + "Mencions y paraules clau (només)" + "En aquesta sala, notifica\'m en" + "Administradors" + "Canvia el meu rol" + "Descendeix a membre" + "Descendeix a moderador" + "Moderació de membres" + "Missatges i contingut" + "Moderadors" + "Restableix permisos" + "Si restableixes els permisos, perdràs la configuració actual." + "Restablir permisos?" + "Rols" + "Detalls de sala" + "Rols i permisos" + "Marca com a llegit" + "Marca com a no llegit" + "Afegeix adreça" + "Tothom ha de sol·licitar l\'accés." + "Sol·licita unir-t\'hi" + "Sí, activa el xifrat" + "Un cop activat, el xifrat d\'una sala no es pot desactivar. L\'històric de missatges només serà visible per als membres de la sala des que van ser convidats o des que s\'hi van unir. +Ningú a part dels membres de la sala podrà llegir els missatges. Això pot impedir que els bots i els ponts (\'bridges\') funcionin correctament. +No es recomana activar el xifrat a les sales que tothom pot trobar i unir-se." + "Vols activar el xifrat?" + "Un cop activat, el xifrat no es pot desactivar." + "Xifrat" + "Activa el xifrat d\'extrem a extrem" + "Tothom pot unir-s\'hi." + "Tothom" + "Només s\'hi poden unir les persones convidades." + "Només amb invitació" + "Accés" + "Actualment els espais no són compatibles" + "És necessària una adreça perquè sigui visible al directori públic." + "Adreça" + "Permet trobar aquesta sala cercant %1$s al directori públic de sales" + "Visible al directori públic" + "Tothom (historial públic)" + "Qui pot llegir l\'historial?" + "Membres, des de quan es van convidar" + "Membres (historial complet)" + "Les adreces de sala són maneres de trobar i accedir a les sales. Això també garanteix que puguis compartir fàcilment la teva sala amb altres persones. +Pots optar per publicar la teva sala al directori públic de sales del teu servidor local." + "Visibilitat" + "Seguretat i privadesa" + diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml index eaf787e4e3..190e1ae5de 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -56,6 +56,7 @@ "Nezavírejte aplikaci, dokud neskončíte." "Příprava pozvánek…" "Pozvat přátele" + "Pozvat" "Opustit konverzaci" "Opustit místnost" "Média a soubory" @@ -134,6 +135,8 @@ "Role" "Podrobnosti místnosti" "Role a oprávnění" + "Označit jako přečtené" + "Označit jako nepřečtené" "Přidat adresu" "Připojit se může kdokoli v autorizovaných prostorách, ale všichni ostatní musí o přístup požádat." "Všichni musí požádat o přístup." diff --git a/features/roomdetails/impl/src/main/res/values-cy/translations.xml b/features/roomdetails/impl/src/main/res/values-cy/translations.xml index be8ab00639..f5d4eecac5 100644 --- a/features/roomdetails/impl/src/main/res/values-cy/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cy/translations.xml @@ -53,6 +53,7 @@ "Peidiwch â chau\'r ap nes ei fod wedi gorffen." "Wrthi\'n paratoi gwahoddiadau…" "Gwahodd pobl" + "Gwahodd" "Gadael y sgwrs" "Gadael yr ystafell" "Cyfryngau a ffeiliau" @@ -119,6 +120,8 @@ "Rolau" "Manylion yr ystafell" "Rolau a chaniatâd" + "Marcio fel wedi\'i ddarllen" + "Marcio fel heb ei ddarllen" "Ychwanegu cyfeiriad ystafell" "Gall unrhyw un ofyn am gael ymuno â\'r ystafell ond bydd rhaid i weinyddwr neu gymedrolwr dderbyn y cais." "Gofyn i gael ymuno" diff --git a/features/roomdetails/impl/src/main/res/values-da/translations.xml b/features/roomdetails/impl/src/main/res/values-da/translations.xml index e381a30957..191df9f059 100644 --- a/features/roomdetails/impl/src/main/res/values-da/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-da/translations.xml @@ -56,6 +56,7 @@ "Luk ikke appen, før den er færdig." "Forbereder invitationer…" "Invitér andre" + "Invitér" "Forlad samtalen" "Forlad rum" "Medier og filer" @@ -131,6 +132,8 @@ "Roller" "Detaljer om rummet" "Roller og tilladelser" + "Marker som læst" + "Marker som ulæst" "Tilføj adresse" "Alle i autoriserede klynger kan deltage, men alle andre skal anmode om adgang." "Alle skal anmode om adgang." @@ -153,7 +156,7 @@ Vi anbefaler ikke at aktivere kryptering for rum, som alle kan finde og deltage "Adgang" "Alle i autoriserede klynger kan deltage." "Alle i %1$s kan deltage." - "Medlemmer af rummet" + "Medlemmer af klyngen" "Klynger understøttes ikke i øjeblikket" "Du skal bruge en adresse for at gøre det synligt i det offentlige register." "Adresse" diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index d810ca0919..08201fdc34 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -56,6 +56,7 @@ "Schließ die App erst, wenn du fertig bist." "Einladungen werden vorbereitet…" "Nutzer einladen" + "Einladen" "Unterhaltung verlassen" "Verlassen" "Medien und Dateien" @@ -131,6 +132,8 @@ "Rollen" "Chat-Details anpassen" "Rollen und Berechtigungen" + "Als gelesen markieren" + "Als ungelesen markieren" "Chat-Adresse hinzufügen" "Jedes Mitglied eines autorisierten Space kann beitreten, aber alle anderen müssen einen Beitritt anfragen." "Zugang nur auf Anfrage." diff --git a/features/roomdetails/impl/src/main/res/values-el/translations.xml b/features/roomdetails/impl/src/main/res/values-el/translations.xml index c1b108a524..5f1a3ce4ab 100644 --- a/features/roomdetails/impl/src/main/res/values-el/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-el/translations.xml @@ -56,6 +56,7 @@ "Μην κλείσετε την εφαρμογή μέχρι να τελειώσει." "Προετοιμασία προσκλήσεων…" "Πρόσκληση ατόμων" + "Πρόσκληση" "Αποχώρηση από τη συζήτηση" "Αποχώρηση από την αίθουσα" "Πολυμέσα και αρχεία" @@ -131,6 +132,8 @@ "Ρόλοι" "Λεπτομέρειες αίθουσας" "Ρόλοι και δικαιώματα" + "Επισήμανση ως αναγνωσμένου" + "Επισήμανση ως μη αναγνωσμένου" "Προσθήκη διεύθυνσης" "Οποιοσδήποτε σε εξουσιοδοτημένους χώρους μπορεί να συμμετάσχει, αλλά όλοι οι άλλοι πρέπει να ζητήσουν πρόσβαση." "Όλοι πρέπει να αιτούνται πρόσβαση." diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index 716e3ed32a..9537596d4e 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -47,6 +47,7 @@ "No se ha podido silenciar esta sala, inténtalo de nuevo." "Error al dejar de silenciar esta sala, por favor inténtalo de nuevo." "Invitar personas" + "Invitar" "Salir de la conversación" "Salir de la sala" "Medios y archivos" @@ -106,6 +107,8 @@ "Roles" "Detalles de la sala" "Roles y permisos" + "Marcar como leído" + "Marcar como no leído" "Agregar dirección" "Todos deben solicitar acceso." "Solicitar unirse" diff --git a/features/roomdetails/impl/src/main/res/values-et/translations.xml b/features/roomdetails/impl/src/main/res/values-et/translations.xml index d5d3af8563..e6962456ac 100644 --- a/features/roomdetails/impl/src/main/res/values-et/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-et/translations.xml @@ -56,6 +56,7 @@ "Ära sulge rakendust enne, kui tegevus on lõppenud." "Valmistan kutseid ette…" "Kutsu osalejaid" + "Kutsu" "Lahku vestlusest" "Lahku jututoast" "Meedia ja failid" @@ -131,6 +132,8 @@ "Rollid" "Jututoa üksikasjad" "Rollid ja õigused" + "Märgi loetuks" + "Märgi mitteloetuks" "Lisa aadress" "Liituda saavad kõik volitatud kogukondade liikmed, kuid kõik teised peavad küsima võimalust ligipääsuks." "Kõik võivad paluda jututoaga liitumist." diff --git a/features/roomdetails/impl/src/main/res/values-eu/translations.xml b/features/roomdetails/impl/src/main/res/values-eu/translations.xml index 7fb07cf2a4..64f32e44d9 100644 --- a/features/roomdetails/impl/src/main/res/values-eu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-eu/translations.xml @@ -44,6 +44,7 @@ "Ezin izan da gela mututu; saiatu berriro." "Ezin izan da gela mututzeari utzi; saiatu berriro." "Gonbidatu jendea" + "Gonbidatu" "Utzi elkarrizketa" "Atera gelatik" "Multimedia eta fitxategiak" @@ -99,6 +100,8 @@ "Rolak" "Gelaren xehetasunak" "Rolak eta baimenak" + "Markatu irakurritzat" + "Markatu irakurri gabetzat" "Gehitu gelaren helbidea" "Bai, gaitu zifratzea" "Zifratzea" diff --git a/features/roomdetails/impl/src/main/res/values-fa/translations.xml b/features/roomdetails/impl/src/main/res/values-fa/translations.xml index bfe743fd1b..af0cbbe29b 100644 --- a/features/roomdetails/impl/src/main/res/values-fa/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fa/translations.xml @@ -4,17 +4,17 @@ "هنگام به‌روز کردن تنظیمات آگاهی خطایی رخ داد." "کارساز خانگیتان از این گزینه در اتاق‌های رمز شده پشتیبانی نمی‌کند. ممکن است در برخی اتاق‌ها آگاه نشوید." "نظرسنجی‌ها" - "فقط مدیران" + "ادمین" "تحریم افراد" - "برداشتن پیام‌ها" - "هرکسی" - "دعوت افراد و پذیرش درخواست‌های پیوستن" - "نظارت اعضا" + "حذف پیام‌ها" + "عضو" + "دعوت کاربران" + "مدیریت اعضا" "پیام‌ها و محتوا" - "مدیرن و ناظران" - "برداشتن افراد و رد درخواست‌های پیوستن" + "ناظم" + "حذف افراد" "تغییر چهرک اتاق" - "ویرایش اتاق" + "ویرایش جزییات" "تغییر نام اتاق" "دگرگونی موضوع اتاق" "فرستادن پیام‌ها" @@ -40,7 +40,7 @@ "رمز شده" "رمزنگاری نشده" "اتاق عمومی" - "ویرایش اتاق" + "ویرایش جزییات" "خطایی ناشناخته رخ داد و اطّلاعات قابل تغییر نبودند." "ناتوان در به‌روز رسانی اتاق" "پیام‌ها با قفل محافظت می‌شوند. فقط شما و گیرندگان، کلیدهای منحصر به فرد برای باز کردن قفل آنها را دارید." @@ -51,6 +51,7 @@ "کاره را تا زمان پایانش نبندید." "آماده سازی دعوت‌ها…" "دعوت افراد" + "دعوت" "ترک گفت‌وگو" "ترک اتاق" "رسانه‌ها و پرونده‌ها" @@ -60,7 +61,7 @@ "پیام‌های سنجاق شده" "نمایه" "درخواست‌های پیوستن" - "نقش‌ها و اجازه‌ها" + "نقش‌ها و مجوزها" "امنیت و محرمانگی" "امنیت" "هم‌رسانی اتاق" @@ -79,8 +80,8 @@ "تحریم نکردن از اتاق" "محروم" "اعضا" - "فقط مدیران" - "مدیرن و ناظران" + "ادمین" + "ناظم" "مالک" "اعضای اتاق" "رفع تحریم %1$s" @@ -111,7 +112,9 @@ "بازنشانی اجازه‌ها؟" "نقش‌ها" "جزییات اتاق" - "نقش‌ها و اجازه‌ها" + "نقش‌ها و مجوزها" + "علامت‌گذاری به عنوان خوانده شده" + "نشان به ناخوانده" "افزودن نشانی اتاق" "درخواست دعوت" "بله. به کار انداختن رمزنگاری" diff --git a/features/roomdetails/impl/src/main/res/values-fi/translations.xml b/features/roomdetails/impl/src/main/res/values-fi/translations.xml index 110790fac1..4eafca087e 100644 --- a/features/roomdetails/impl/src/main/res/values-fi/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fi/translations.xml @@ -56,6 +56,7 @@ "Älä sulje sovellusta ennen kuin se on valmis." "Valmistellaan kutsuja…" "Kutsu henkilöitä" + "Kutsu" "Poistu keskustelusta" "Poistu huoneesta" "Media ja tiedostot" @@ -131,6 +132,8 @@ "Roolit" "Huoneen tiedot" "Roolit ja oikeudet" + "Merkitse luetuksi" + "Merkitse lukemattomaksi" "Lisää osoite" "Kuka tahansa valtuutetuissa tiloissa voi liittyä, mutta kaikkien muiden on pyydettävä pääsyä." "Kaikkien on pyydettävä pääsyä." diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml index 0fad68c934..0d6844cb7e 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -56,6 +56,7 @@ "Ne fermez pas l’application avant que l’opération soit terminée." "Préparation des invitations…" "Inviter des amis" + "Inviter" "Quitter la discussion" "Quitter le salon" "Médias et fichiers" @@ -131,6 +132,8 @@ "Rôles" "Détails du salon" "Rôles & autorisations" + "Marquer comme lu" + "Marquer comme non lu" "Ajouter une adresse" "Toute personne se trouvant dans un espace autorisé peut participer, mais toutes les autres doivent demander l’accès." "Tout le monde doit demander un accès." diff --git a/features/roomdetails/impl/src/main/res/values-hr/translations.xml b/features/roomdetails/impl/src/main/res/values-hr/translations.xml index 1bc264ca10..5619164dd2 100644 --- a/features/roomdetails/impl/src/main/res/values-hr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hr/translations.xml @@ -56,6 +56,7 @@ "Ne zatvarajte aplikaciju dok se ne završi." "Priprema pozivnica…" "Pozovi osobe" + "Pozovi" "Napusti razgovor" "Napusti sobu" "Mediji i datoteke" @@ -134,6 +135,8 @@ "Uloge" "Pojedinosti o sobi" "Uloge i dopuštenja" + "Označi kao pročitano" + "Označi kao nepročitano" "Dodaj adresu" "Svatko tko se nalazi u ovlaštenim prostorima može se pridružiti, ali svi ostali moraju zatražiti pristup." "Svi moraju zatražiti pristup." diff --git a/features/roomdetails/impl/src/main/res/values-hu/translations.xml b/features/roomdetails/impl/src/main/res/values-hu/translations.xml index 1dc523f319..fed765fb45 100644 --- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml @@ -56,6 +56,7 @@ "Ne zárja be az alkalmazást, amíg nem végzett." "Meghívók előkészítése…" "Ismerősök meghívása" + "Meghívás" "Beszélgetés elhagyása" "Szoba elhagyása" "Média és fájlok" @@ -131,6 +132,8 @@ "Szerepkörök" "Szoba részletei" "Szerepkörök és jogosultságok" + "Megjelölés olvasottként" + "Megjelölés olvasatlanként" "Cím hozzáadása" "Bárki csatlakozhat, az engedélyezett terekből, és mindenki másnak hozzáférést kell kérnie." "Mindenkinek hozzáférést kell kérnie." diff --git a/features/roomdetails/impl/src/main/res/values-in/translations.xml b/features/roomdetails/impl/src/main/res/values-in/translations.xml index 41bf3d2826..d7403329c4 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -53,6 +53,7 @@ "Jangan tutup aplikasi tunggu hingga selesai." "Mempersiapkan undangan…" "Undang orang-orang" + "Undang" "Tinggalkan percakapan" "Tinggalkan ruangan" "Media dan berkas" @@ -114,6 +115,8 @@ "Peran" "Detail ruangan" "Peran dan perizinan" + "Tandai sebagai dibaca" + "Tandai sebagai belum dibaca" "Tambahkan alamat" "Meminta hak akses pada administrator atau moderator." "Ijin bergabung" 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 22ec99f2b5..b8a18d4421 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -56,6 +56,7 @@ "Non chiudere l\'app fino al completamento." "Preparazione degli inviti…" "Invita persone" + "Invita" "Abbandona la conversazione" "Esci dalla stanza" "File e contenuti multimediali" @@ -131,6 +132,8 @@ "Ruoli" "Dettagli della stanza" "Ruoli e autorizzazioni" + "Segna come letto" + "Segna come non letto" "Aggiungi indirizzo" "Chiunque si trovi in spazi autorizzati può partecipare, ma tutti gli altri devono richiedere l\'accesso." "Chiunque deve richiedere l\'accesso." diff --git a/features/roomdetails/impl/src/main/res/values-ja/translations.xml b/features/roomdetails/impl/src/main/res/values-ja/translations.xml index 70b3255437..5a65f1a404 100644 --- a/features/roomdetails/impl/src/main/res/values-ja/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ja/translations.xml @@ -56,6 +56,7 @@ "終了するまでアプリを閉じないでください。" "招待を準備中…" "ユーザーを招待" + "招待" "会話を退出" "ルームを退出" "ファイルとメディア" @@ -128,6 +129,8 @@ "役割" "ルームの詳細" "役割と権限" + "既読にする" + "未読にする" "アドレスを追加" "認証済みのスペースに所属するユーザーのみが参加できます。それ以外のユーザーは参加へのリクエストが必要です。" "参加のリクエストが必須です。" diff --git a/features/roomdetails/impl/src/main/res/values-ka/translations.xml b/features/roomdetails/impl/src/main/res/values-ka/translations.xml index 3ffc962603..8fd0b2c8a6 100644 --- a/features/roomdetails/impl/src/main/res/values-ka/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ka/translations.xml @@ -39,6 +39,7 @@ "ამ ოთახის დადუმება ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "ამ ოთახის დადუმების მოხსნა ვერ მოხერხდა. გთხოვთ, სცადოთ ხელახლა." "ხალხის მოწვევა" + "მოწვევა" "საუბრის დატოვება" "ოთახის დატოვება" "მორგებული" @@ -93,4 +94,6 @@ "როლები" "ოთახის დეტალები" "როლები და ნებართვები" + "წაკითხულად მონიშვნა" + "წაუკითხავად მონიშვნა" diff --git a/features/roomdetails/impl/src/main/res/values-ko/translations.xml b/features/roomdetails/impl/src/main/res/values-ko/translations.xml index 2c6462c4a7..4a32cab45d 100644 --- a/features/roomdetails/impl/src/main/res/values-ko/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ko/translations.xml @@ -56,6 +56,7 @@ "작업이 완료될 때까지 앱을 닫지 마세요." "초대 준비중…" "사람 초대하기" + "초대" "대화에서 나가기" "방 떠나기" "미디어 및 파일" @@ -128,6 +129,8 @@ "역할" "방 세부 정보" "역할 및 권한" + "읽음으로 표시" + "읽지 않음으로 표시" "주소 추가" "승인된 스페이스의 멤버는 누구나 참여할 수 있지만, 그 외의 인원은 액세스 요청을 해야 합니다." "모든 사용자가 액세스 권한을 요청해야 합니다." diff --git a/features/roomdetails/impl/src/main/res/values-lt/translations.xml b/features/roomdetails/impl/src/main/res/values-lt/translations.xml index 84f74042da..9bdaeb86e5 100644 --- a/features/roomdetails/impl/src/main/res/values-lt/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-lt/translations.xml @@ -8,6 +8,7 @@ "Žinutės yra užrakintos. Tik Jūs ir gavėjai turite unikalius raktus joms atrakinti." "Įjungtas žinučių šifravimas" "Pakviesti žmonių" + "Kviesti" "Palikti pokalbį" "Išeiti iš kambario" "Pasirinktinis" diff --git a/features/roomdetails/impl/src/main/res/values-nb/translations.xml b/features/roomdetails/impl/src/main/res/values-nb/translations.xml index e522f5cd79..0cb3dfc268 100644 --- a/features/roomdetails/impl/src/main/res/values-nb/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nb/translations.xml @@ -56,6 +56,7 @@ "Ikke lukk appen før den er ferdig." "Forbereder invitasjoner…" "Inviter folk" + "Inviter" "Forlat samtalen" "Forlat rommet" "Medier og filer" @@ -131,6 +132,8 @@ "Roller" "Romdetaljer" "Roller og tillatelser" + "Marker som lest" + "Merk som ulest" "Legg til adresse" "Alle i autoriserte områder kan bli med, men alle andre må be om tilgang." "Alle må be om tilgang." diff --git a/features/roomdetails/impl/src/main/res/values-nl/translations.xml b/features/roomdetails/impl/src/main/res/values-nl/translations.xml index 9234921c35..2ee5caca97 100644 --- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml @@ -43,6 +43,7 @@ "Het dempen van deze kamer is mislukt. Probeer het opnieuw." "Het dempen opheffen voor deze kamer is mislukt. Probeer het opnieuw." "Mensen uitnodigen" + "Uitnodigen" "Gesprek verlaten" "Kamer verlaten" "Media en bestanden" @@ -99,4 +100,6 @@ "Rollen" "Kamergegevens" "Rollen en rechten" + "Markeren als gelezen" + "Markeren als ongelezen" diff --git a/features/roomdetails/impl/src/main/res/values-pl/translations.xml b/features/roomdetails/impl/src/main/res/values-pl/translations.xml index e22636290b..2979fcacdf 100644 --- a/features/roomdetails/impl/src/main/res/values-pl/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pl/translations.xml @@ -1,19 +1,24 @@ - "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." - "Adres pokoju" + "Nowi członkowie nie widzą historii" + "Nowi członkowie widzą historię" + "Każdy może przeglądać historię" + "Aby pokój był widoczny w katalogu pokoi publicznych, potrzebny jest adres pokoju." + "Edytuj adres" "Wystąpił błąd podczas aktualizacji ustawienia powiadomień." "Twój serwer domowy nie wspiera tej opcji w pokojach szyfrowanych, możesz nie otrzymać powiadomień z niektórych pokoi." "Ankiety" - "Tylko administratorzy" + "Administrator" "Banowanie osób" "Usuń wiadomości" - "Zapraszanie osób i akceptowanie próśb o dołączenie" + "Członek" + "Zaproś osoby" + "Zarządzaj członkami" "Wiadomości i zawartość" - "Administratorzy i moderatorzy" - "Usuwanie osób i odrzucanie próśb o dołączenie" + "Moderator" + "Usuń osoby" "Zmień awatar pokoju" - "Edytuj pokój" + "Edytuj szczegóły" "Zmień nazwę pokoju" "Zmień temat pokoju" "Wysyłanie wiadomości" @@ -26,7 +31,7 @@ "Nie będzie można cofnąć tej zmiany, jeśli się zdegradujesz. Jeśli jesteś ostatnim uprzywilejowanym użytkownikiem w pokoju, nie będziesz w stanie odzyskać uprawnień." "Zdegradować siebie?" "%1$s (Oczekujące)" - "(Oczekujący)" + "(Oczekujące)" "Administratorzy automatycznie mają uprawnienia moderatora" "Właściciele automatycznie mają uprawnienia administratora." "Edytuj moderatorów" @@ -40,7 +45,7 @@ "Szyfrowany" "Nieszyfrowany" "Pokój publiczny" - "Edytuj pokój" + "Edytuj szczegóły" "Wystąpił nieznany błąd i nie można było zmienić informacji." "Nie można zaktualizować pokoju" "Wiadomości są zabezpieczone kłódkami. Tylko Ty i odbiorcy macie unikalne klucze do ich odblokowania." @@ -51,6 +56,7 @@ "Nie zamykaj aplikacji przed zakończeniem." "Przygotowywanie zaproszeń…" "Zaproś znajomych" + "Zaproś" "Opuść rozmowę" "Opuść pokój" "Media i pliki" @@ -61,13 +67,21 @@ "Profil" "Prośby o dołączenie" "Role i uprawnienia" + "Nazwa" "Bezpieczeństwo i prywatność" "Bezpieczeństwo" "Udostępnij pokój" "Informacje pokoju" "Temat" "Aktualizuję pokój…" - "W tym pokoju nie ma zbanowanych użytkowników." + "Nie ma zbanowanych użytkowników." + + "%1$d zbanowany" + "%1$d zbanowanych" + "%1$d zbanowanych" + + "Sprawdź pisownię lub wyszukaj ponownie" + "Brak wyników dla “%1$s”" "%1$d osoba" "%1$d osoby" @@ -80,16 +94,22 @@ "Odbanuj z pokoju" "Zbanowanych" "Członków" - "Tylko administratorzy" - "Administratorzy i moderatorzy" + + "%1$d zaproszony" + "%1$d zaproszonych" + "%1$d zaproszonych" + + "Oczekuje" + "Administrator" + "Moderator" "Właściciel" "Członkowie pokoju" "Odbanowanie %1$s" "Zezwalaj na ustawienia niestandardowe" "Włączenie tej opcji nadpisze ustawienie domyślne" "Powiadamiaj mnie o tym czacie przez" - "Możesz to zmienić w swoim %1$s." - "ustawienia globalne" + "Możesz to zmienić w %1$s." + "ustawieniach globalnych" "Ustawienie domyślne" "Usuń ustawienia własne" "Wystąpił błąd podczas ładowania ustawień powiadomień." @@ -108,15 +128,20 @@ "Wiadomości i zawartość" "Moderatorzy" "Właściciele" - "Resetuj uprawnienia" + "Uprawnienia" + "Zresetuj uprawnienia" "Po zresetowaniu uprawnień utracisz bieżące ustawienia." "Zresetować uprawnienia?" "Role" "Szczegóły pokoju" "Role i uprawnienia" - "Dodaj adres pokoju" - "Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić żądanie." + "Oznacz jako przeczytane" + "Oznacz jako nieprzeczytane" + "Dodaj adres" + "Każdy w autoryzowanych przestrzeniach może dołączyć, ale wszyscy inni muszą poprosić o dostęp." + "Każdy musi poprosić o dostęp." "Poproś o dołączenie" + "Każdy w %1$s może dołączyć, ale wszyscy pozostali muszą poprosić o dostęp." "Tak, włącz szyfrowanie" "Po włączeniu szyfrowanie pokoju nie może zostać wyłączone, a historia wiadomości będzie widoczna tylko dla członków od momentu, w którym dołączyli lub zostali zaproszeni. Nikt poza członkami pokoju nie będzie mógł czytać wiadomości. Może to wpłynąć na prawidłowe działanie botów lub mostków. @@ -125,23 +150,31 @@ Odradzamy włączanie szyfrowania dla pokoi, które każdy może znaleźć i do "Po włączeniu szyfrowania nie można wyłączyć." "Szyfrowanie" "Włącz szyfrowanie end-to-end" - "Każdy może znaleźć i dołączyć" + "Każdy może dołączyć." "Każdy" - "Tylko osoby z zaproszeniem mogą dołączyć" - "Tylko zaproszenie" - "Dostęp do pokoju" + "Wybierz, którzy członkowie przestrzeni mogą dołączyć do tego pokoju bez zaproszenia. %1$s" + "Zarządzaj przestrzeniami" + "Tylko zaproszone osoby mogą dołączyć" + "Tylko na zaproszenie" + "Dostęp" + "Każdy w autoryzowanych przestrzeniach może dołączyć." + "Każdy w %1$s może dołączyć." + "Członkowie przestrzeni" "Przestrzenie nie są obecnie wspierane" - "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." - "Adres pokoju" + "Aby pokój był widoczny w katalogu pokoi publicznych, potrzebny jest adres pokoju." + "Adres" "Zezwól na znalezienie tego pokoju wyszukując %1$s w katalogu pokoi publicznych" + "Zezwól, by inni mogli Cię znaleźć, przeszukując katalog publiczny." "Widoczny w katalogu pokoi publicznych" - "Ktokolwiek" + "Każdy (historia jest publiczna)" + "Zmiany nie zmienią przeszłych wiadomości, tylko nowe. %1$s" "Kto może czytać historię" - "Od momentu kiedy członkowie zostali zaproszeni" - "Członkowie od momentu włączenia tej opcji" + "Członkowie od kiedy zostali zaproszeni" + "Członkowie (cała historia)" "Adresy pokoju umożliwiają łatwe znalezienie i dołączenie do pokojów. Również możesz się zdecydować na upublicznienie Twojego serwera w katalogu pokoi publicznych." "Publikowanie pokoju" - "Widoczność pokoju" + "Adresy pokoi pomagają w znalezieniu i dołączeniu do pokoi i przestrzeni. Umożliwiają również łatwe udostępnianie ich innym." + "Widoczność" "Bezpieczeństwo i prywatność" diff --git a/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml b/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml index ddbbe0f601..f2ddfb86cb 100644 --- a/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pt-rBR/translations.xml @@ -53,6 +53,7 @@ "Não feche o aplicativo até terminar." "Preparando convites…" "Convidar pessoas" + "Convidar" "Sair da conversa" "Sair da sala" "Mídia e arquivos" @@ -128,6 +129,8 @@ "Cargos" "Detalhes da sala" "Cargos e permissões" + "Marcar como lida" + "Marcar como não lida" "Adicionar endereço" "Qualquer um nos espaços autorizados podem entrar, mas todos os outros devem pedir acesso." "Qualquer um pode pedir acesso, mas um administrador terá que aceitar o pedido." diff --git a/features/roomdetails/impl/src/main/res/values-pt/translations.xml b/features/roomdetails/impl/src/main/res/values-pt/translations.xml index 592cba34f6..04ded84480 100644 --- a/features/roomdetails/impl/src/main/res/values-pt/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-pt/translations.xml @@ -53,6 +53,7 @@ "Não feches a aplicação até concluir." "A preparar convites…" "Convidar pessoas" + "Convidar" "Sair da conversa" "Sair da sala" "Multimédia e ficheiros" @@ -127,6 +128,8 @@ "Cargos" "Detalhes da sala" "Cargos e permissões" + "Marcar como lida" + "Marcar como não lida" "Adicionar endereço" "Todos precisam de pedir acesso." "Pedir para entrar" diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index 2eac873575..f3feda811a 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,8 @@ + "Membrii noi nu pot vedea istoricul" + "Membrii noi pot vedea istoricul" + "Oricine poate vedea istoricul" "Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public." "Editați adresa" "A apărut o eroare în timpul actualizării setărilor pentru notificari." @@ -53,6 +56,7 @@ "Nu închideți aplicația până nu se termină." "Se pregătesc invitațiile…" "Invitați prieteni" + "Invitați" "Părăsiți conversația" "Părăsiți camera" "Media și fișiere" @@ -131,9 +135,12 @@ "Roluri" "Detaliile camerei" "Roluri și permisiuni" + "Marcați ca citită" + "Marcați ca necitită" "Adăugați o adresă" "Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul." "Toată lumea trebuie să solicite acces." + "Solicitați să vă alăturați" "Oricine în %1$s se poate alătura, dar toți ceilalți trebuie să solicite acces." "Da, activați criptarea" "Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei. @@ -144,22 +151,26 @@ Nu recomandăm activarea criptării pentru camerele pe care oricine le poate gă "Criptare" "Activați criptarea end-to-end" "Oricine se poate alătura." - "Alegeți membrii căror spații se pot alătura acestei camere fără invitație. %1$s" + "Oricine" + "Alegeți membrii căror spații se pot alătura acestei cameră fără invitație. %1$s" "Gestionați spațiile" "Doar persoanele invitate se pot alătura." "Doar pe bază de invitație" "Acces" "Oricine se află într-un spațiu autorizat poate participa." "Oricine din %1$s se poate alătura." + "Membrii spațiului" "Spațiile nu sunt momentan suportate." "Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public." "Adresă" "Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s" "Permiteți găsirea prin căutarea în directorul public." "Vizibilă în directorul de camere publice" + "Oricine (istoricul este public)" + "Modificările nu vor afecta mesajele anterioare, ci doar pe cele noi. %1$s" "Cine poate citi mesajele anterioare" - "Doar pentru membri, de la momentul în care au fost invitați" - "Doar pentru membri, după selectarea acestei opțiuni" + "Membri de la momentul invitației" + "Membri (istoric complet)" "Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane. Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră." "Publicare cameră" diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml index 7ad44fff9d..ebe1448aa9 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -6,7 +6,7 @@ "Необходимо задать адрес комнаты, чтобы опубликовать ее в каталоге комнат." "Редактировать адрес комнаты" "Произошла ошибка при обновлении настройки уведомления." - "Ваш сервер не поддерживает эту опцию для зашифрованных комнат, вы можете не получать некоторые уведомления." + "Ваш сервер не поддерживает эту опцию для зашифрованных комнат, Вы можете не получать некоторые уведомления." "Опросы" "Администраторы" "Блокировать пользователей" @@ -23,7 +23,7 @@ "Менять тему комнаты" "Отправлять сообщения" "Редактировать администраторов" - "Вы не сможете отменить это действие. Вы даете пользователю такой же уровень прав, как и у вас" + "Вы не сможете отменить это действие. Вы даете пользователю такой же уровень прав, как и у Вас" "Добавить администратора?" "Отменить данное действие будет невозможно. Права передадутся выбранным пользователям. После вашего выхода действие станет необратимым." "Передать владение?" @@ -39,7 +39,7 @@ "Администраторы" "Модераторы" "Участники" - "У вас есть несохраненные изменения." + "У Вас есть несохраненные изменения." "Сохранить изменения?" "Добавить тему" "Зашифровано" @@ -51,18 +51,19 @@ "Сообщения зашифрованы. Только вы и ваши собеседники имеют доступ к сообщениям." "Шифрование сообщений включено" "Произошла ошибка при загрузке настроек уведомлений." - "Не удалось отключить звук в этой комнате, попробуйте еще раз." - "Не удалось включить звук в эту комнату, попробуйте еще раз." + "Не удалось отключить звук в этой комнате, попробуйте ещё раз." + "Не удалось включить звук в этой комнате, попробуйте ещё раз." "Не закрывайте приложение, пока не закончите." "Подготовка приглашений…" "Пригласить в комнату" + "Пригласить" "Покинуть беседу" "Покинуть комнату" "Медиа и файлы" "Пользовательские" "По умолчанию" "Уведомления" - "Закрепленные сообщения" + "Закреплённые сообщения" "Профиль" "Запросы на вступление" "Роли и разрешения" @@ -112,8 +113,8 @@ "Настройка по умолчанию" "Удалить пользовательскую настройку" "Произошла ошибка при загрузке настроек уведомлений." - "Не удалось восстановить режим по умолчанию, попробуйте еще раз." - "Не удалось настроить режим, попробуйте еще раз." + "Не удалось восстановить режим по умолчанию, попробуйте ещё раз." + "Не удалось настроить режим, попробуйте ещё раз." "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате." "О всех сообщениях" "Только упоминания и ключевые слова" @@ -134,6 +135,8 @@ "Роли" "Информация о комнате" "Роли и разрешения" + "Пометить как прочитанное" + "Отметить как непрочитанное" "Добавить адрес" "Кто угодно из авторизованных пространств может присоединиться, а всем остальным необходимо запросить доступ." "Каждый должен запросить доступ." @@ -150,7 +153,7 @@ "Любой желающий может присоединиться" "Кто угодно" "Выберите, участники каких пространств могут присоединиться к этой комнате без приглашения.%1$s" - "Управление пространством" + "Управление пространствами" "Присоединиться могут только приглашенные люди." "Только по приглашению" "Доступ" diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml index 225dcedac3..122c27e058 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -53,6 +53,7 @@ "Nezatvárajte aplikáciu, kým sa neukončí pozývanie." "Príprava pozvánok…" "Pozvať ľudí" + "Pozvať" "Opustiť konverzáciu" "Opustiť miestnosť" "Médiá a súbory" @@ -131,6 +132,8 @@ "Roly" "Podrobnosti o miestnosti" "Roly a povolenia" + "Označiť ako prečítané" + "Označiť ako neprečítané" "Pridať adresu" "Pripojiť sa môže ktokoľvek z autorizovaných priestorov, ale všetci ostatní musia o prístup požiadať." "Všetci musia požiadať o prístup." diff --git a/features/roomdetails/impl/src/main/res/values-sv/translations.xml b/features/roomdetails/impl/src/main/res/values-sv/translations.xml index 795dc97106..aa9f244498 100644 --- a/features/roomdetails/impl/src/main/res/values-sv/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml @@ -53,6 +53,7 @@ "Stäng inte appen förrän det är klart." "Förbereder inbjudningar …" "Bjud in personer" + "Bjud in" "Lämna konversation" "Lämna rum" "Media och filer" @@ -115,6 +116,8 @@ "Roller" "Rumsdetaljer" "Roller och behörigheter" + "Markera som läst" + "Markera som oläst" "Lägg till adress" "Alla måste begära åtkomst." "Be om att gå med" diff --git a/features/roomdetails/impl/src/main/res/values-tr/translations.xml b/features/roomdetails/impl/src/main/res/values-tr/translations.xml index 16137ce753..a9bfc6fbe2 100644 --- a/features/roomdetails/impl/src/main/res/values-tr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-tr/translations.xml @@ -46,6 +46,7 @@ "Bu odayı sessize alma başarısız oldu, lütfen tekrar deneyin." "Bu odanın sesi açılamadı, lütfen tekrar deneyin." "Kişileri davet et" + "Davet et" "Sohbeti bırak" "Odadan ayrıl" "Medya ve dosyalar" @@ -112,6 +113,8 @@ "Roller" "Oda bilgileri" "Roller ve izinler" + "Okundu olarak işaretle" + "Okunmamış olarak işaretle" "Oda adresi ekle" "Yetkilendirilmiş alanlardaki herkes katılabilir, diğer herkes erişim talep etmelidir." "Herkes odaya katılma isteğinde bulunabilir ancak bir yönetici veya moderatörün isteği kabul etmesi gerekir." diff --git a/features/roomdetails/impl/src/main/res/values-uk/translations.xml b/features/roomdetails/impl/src/main/res/values-uk/translations.xml index 46adfa54d5..8f566bb235 100644 --- a/features/roomdetails/impl/src/main/res/values-uk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml @@ -1,5 +1,8 @@ + "Нові учасники не бачать історії" + "Нові учасники бачать історію" + "Будь-хто може переглянути історію" "Вам знадобиться адреса кімнати, щоб зробити її видимою в каталозі." "Змінити адресу" "Під час оновлення налаштувань сповіщень сталася помилка." @@ -53,6 +56,7 @@ "Не закривайте застосунок доки не завершите." "Приготування запрошень…" "Запросити людей" + "Запросити" "Залишити розмову" "Вийти з кімнати" "Медіа та файли" @@ -71,6 +75,11 @@ "Тема" "Оновлення кімнати…" "Немає заблокованих користувачів." + + "%1$d Заблокований" + "%1$d Заблоковано" + "%1$d Заблоковано" + "Перевірте правопис або спробуйте новий пошук" "Немає результатів за запитом «%1$s»" @@ -126,9 +135,13 @@ "Ролі" "Деталі кімнати" "Ролі та дозволи" + "Позначити прочитаним" + "Позначити непрочитаним" "Додати адресу" + "Будь-хто в авторизованих просторах може приєднатися, але всі інші повинні подати запит на доступ." "Усі повинні запитувати доступ." "Запит на приєднання" + "Будь-хто з %1$s може приєднатися, але всі інші повинні подати запит на доступ." "Так, увімкнути шифрування" "Після ввімкнення шифрування кімнати, його неможливо вимкнути, історію повідомлень бачитимуть лише учасники кімнати, яких було запрошено або які приєдналися до кімнати. Ніхто, крім учасників кімнати, не зможе прочитати повідомлення. Це може перешкоджати коректній роботі ботів і мостів. @@ -144,11 +157,14 @@ "Приєднатися можуть лише запрошені люди." "Лише запрошені" "Доступ" + "Долучитися може будь-хто, хто має доступ до авторизованих просторів." "Долучитися може будь-хто з %1$s." + "Учасники простору" "Простори наразі не підтримуються" "Вам знадобиться адреса кімнати, щоб зробити її видимою в каталозі." "Адреса" "Дозвольте, щоб цю кімнату можна було знайти за допомогою пошуку в каталозі загальнодоступних кімнат %1$s " + "Дозвольте знаходити вас за допомогою пошуку в публічному каталозі." "Видима в загальному каталозі" "Будь-хто (загальнодоступна історія)" "Зміни не вплинуть на попередні повідомлення, лише на нові. %1$s" @@ -158,6 +174,7 @@ "Адреси кімнат — це спосіб знайти кімнату та отримати до неї доступ. Це також гарантує, що ви можете легко поділитися своєю кімнатою з іншими. Ви можете опублікувати свою кімнату в каталозі загальнодоступних кімнат вашого домашнього сервера." "Публікація в кімнаті" + "Адреси — це спосіб знаходити кімнати та простори та отримувати до них доступ. Це також гарантує, що ви зможете легко ділитися ними з іншими." "Видимість" "Безпека й приватність" diff --git a/features/roomdetails/impl/src/main/res/values-ur/translations.xml b/features/roomdetails/impl/src/main/res/values-ur/translations.xml index 3715bb91ff..5c32811f36 100644 --- a/features/roomdetails/impl/src/main/res/values-ur/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ur/translations.xml @@ -43,6 +43,7 @@ "اس کمرے کو خاموش کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" "اس کمرے کو غیر خاموش کرنے میں ناکام، برائے مہربانی دوبارہ کوشش کریں۔" "لوگوں کو مدعو کریں" + "مدعو کریں" "گفتگو چھوڑیں" "کمرہ چھوڑ دیں" "حسب ضرورت" @@ -98,4 +99,6 @@ "کردارہا" "کمرے کی تفصیلات" "کردارہا اور اجازتیں" + "بطور مقروءہ نشانزد کریں" + "بطور غیر مقروءہ نشانزد کریں" diff --git a/features/roomdetails/impl/src/main/res/values-uz/translations.xml b/features/roomdetails/impl/src/main/res/values-uz/translations.xml index 1b4718ee44..d7778f2df4 100644 --- a/features/roomdetails/impl/src/main/res/values-uz/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uz/translations.xml @@ -56,6 +56,7 @@ "Tugallanmaguncha ilovani yopmang." "Taklifnomalar tayyorlanmoqda…" "Odamlarni taklif qiling" + "Taklif qilish" "Suhbatni tark etish" "Xonani tark etish" "Media va fayllar" @@ -131,6 +132,8 @@ "Rollar" "Xona tafsilotlari" "Rollar va ruxsatlar" + "Oʻqilgan deb belgilash" + "Oʻqilmagan deb belgilash" "Xona manzilini kiritish" "Vakolatli guruhlardagi har kim qo‘shilishi mumkin, lekin qolganlar ruxsat so‘rashi kerak. Tarjima eslatmasi yo‘q" "Xonaga qo‘shilishni istalgan kishi so‘rashi mumkin, lekin administrator yoki moderator so‘rovni qabul qilishi kerak" diff --git a/features/roomdetails/impl/src/main/res/values-vi/translations.xml b/features/roomdetails/impl/src/main/res/values-vi/translations.xml index ac37e6fcfa..ecc7e4e227 100644 --- a/features/roomdetails/impl/src/main/res/values-vi/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-vi/translations.xml @@ -46,6 +46,7 @@ "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 đó" + "Mời" "Rời khỏi cuộc trò chuyện" "Rời phòng" "Tùy chỉnh" @@ -105,6 +106,8 @@ "Vai trò" "Chi tiết phòng" "Vai trò và quyền hạn" + "Đánh dấu đã đọc" + "Đánh dấu chưa đọc" "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." diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index 1cbabe0e54..0e516d0c29 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -56,6 +56,7 @@ "完成前請勿關閉應用程式。" "正在準備邀請……" "邀請夥伴" + "邀請" "離開對話" "離開聊天室" "媒體與檔案" @@ -128,6 +129,8 @@ "身份" "聊天室資訊" "角色與權限" + "標為已讀" + "標為未讀" "新增地址" "任何在授權空間的人都可以加入,但其他人都必須提出申請。" "所有人都必須申請存取權。" 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 d2cc6773fb..8a5c5734ed 100644 --- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml @@ -9,7 +9,7 @@ "主服务器不支持在加密房间中的此选项,因此在某些房间你可能无法收到通知。" "投票" "管理员" - "封禁成员" + "封禁人员" "移除消息" "成员" "邀请人员" @@ -55,7 +55,8 @@ "无法取消静音此房间,请重试。" "完成之前请勿关闭 app。" "正在准备邀请…" - "邀请朋友" + "邀请人员" + "邀请" "离开聊天" "离开房间" "媒体与文件" @@ -75,7 +76,7 @@ "正在更新房间…" "暂无被封禁的用户。" - "%1$d 被禁用" + "%1$d 人被封禁" "检查拼写或尝试新搜索" "未找到 “%1$s” 相关结果" @@ -128,6 +129,8 @@ "角色" "房间详细信息" "角色与权限" + "设为已读" + "设为未读" "添加地址" "已授权空间内的任何成员都可以加入,其他人必须申请访问。" "所有用户均需申请访问权限。" @@ -145,7 +148,7 @@ "任何人" "选择哪些无需邀请即可加入此房间的空间成员。%1$s" "管理空间" - "仅限受邀者加入。" + "仅限受邀人员加入。" "仅限受邀者" "访问权限" "任何位于已授权空间的成员均可加入。" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 6275b2837e..0287b5657b 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -56,6 +56,7 @@ "Don\'t close the app until finished." "Preparing invitations…" "Invite people" + "Invite" "Leave conversation" "Leave room" "Media and files" @@ -131,6 +132,8 @@ "Roles" "Room details" "Roles & permissions" + "Mark as read" + "Mark as unread" "Add address" "Anyone in authorised spaces can join, but everyone else must request access." "Everyone must request access." 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 bcf25b2aac..9a97380d21 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 @@ -68,9 +68,10 @@ class DefaultRoomDetailsEntryPointTest { ) } val callback = object : RoomDetailsEntryPoint.Callback { + override fun onDone() = lambdaError() override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToDeveloperSettings() = lambdaError() - override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() + override fun navigateToRoom(roomId: RoomId, serverNames: List, clearBackStack: Boolean) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/FakeRoomDetailsNavigator.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/FakeRoomDetailsNavigator.kt new file mode 100644 index 0000000000..b416902dab --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/FakeRoomDetailsNavigator.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.roomdetails.impl + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeRoomDetailsNavigator( + private val onDoneResult: () -> Unit = { lambdaError() } +) : RoomDetailsNavigator { + override fun onDone() = onDoneResult() +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt index 2d85744345..edb9115a7e 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/MatrixRoomFixture.kt @@ -15,6 +15,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.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -28,7 +29,7 @@ import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.tests.testutils.lambda.lambdaError -fun aRoom( +fun aFakeBaseRoom( sessionId: SessionId = A_SESSION_ID, roomId: RoomId = A_ROOM_ID, displayName: String = A_ROOM_NAME, @@ -49,6 +50,7 @@ fun aRoom( getUpdatedMemberResult: (UserId) -> Result = { lambdaError() }, userRoleResult: () -> Result = { lambdaError() }, setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, + markAsReadResult: (ReceiptType) -> Result = { lambdaError() }, ) = FakeBaseRoom( sessionId = sessionId, roomId = roomId, @@ -57,6 +59,7 @@ fun aRoom( getUpdatedMemberResult = getUpdatedMemberResult, userRoleResult = userRoleResult, setIsFavoriteResult = setIsFavoriteResult, + markAsReadResult = markAsReadResult, roomPermissions = roomPermissions, initialRoomInfo = aRoomInfo( name = displayName, @@ -106,6 +109,7 @@ fun aJoinedRoom( publishRoomAliasInRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, removeRoomAliasFromRoomDirectoryResult: (RoomAlias) -> Result = { lambdaError() }, setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, + markAsReadResult: (ReceiptType) -> Result = { lambdaError() }, ) = FakeJoinedRoom( roomNotificationSettingsService = notificationSettingsService, setNameResult = setNameResult, @@ -118,7 +122,7 @@ fun aJoinedRoom( updateCanonicalAliasResult = updateCanonicalAliasResult, publishRoomAliasInRoomDirectoryResult = publishRoomAliasInRoomDirectoryResult, removeRoomAliasFromRoomDirectoryResult = removeRoomAliasFromRoomDirectoryResult, - baseRoom = aRoom( + baseRoom = aFakeBaseRoom( sessionId = sessionId, roomId = roomId, roomPermissions = roomPermissions, @@ -139,5 +143,6 @@ fun aJoinedRoom( joinedMemberCount = joinedMemberCount, activeMemberCount = activeMemberCount, invitedMemberCount = invitedMemberCount, + markAsReadResult = markAsReadResult, ) ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt index d355010307..51a6f97ab0 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt @@ -21,9 +21,8 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers -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.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembersState @@ -31,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -44,7 +44,11 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore +import io.element.android.libraries.push.api.notifications.NotificationCleaner +import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.EventsRecorder @@ -80,14 +84,12 @@ class RoomDetailsPresenterTest { dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), analyticsService: AnalyticsService = FakeAnalyticsService(), - featureFlagService: FeatureFlagService = FakeFeatureFlagService( - mapOf( - FeatureFlags.Knock.key to false, - ) - ), encryptionService: FakeEncryptionService = FakeEncryptionService(), clipboardHelper: ClipboardHelper = FakeClipboardHelper(), - appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore() + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + navigator: RoomDetailsNavigator = FakeRoomDetailsNavigator(), + notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), + sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { @@ -104,9 +106,9 @@ class RoomDetailsPresenterTest { } } return RoomDetailsPresenter( + navigator = navigator, client = matrixClient, room = room, - featureFlagService = featureFlagService, notificationSettingsService = matrixClient.notificationSettingsService, roomMembersDetailsPresenterFactory = roomMemberDetailsPresenterFactory, leaveRoomPresenter = { leaveRoomState }, @@ -115,6 +117,8 @@ class RoomDetailsPresenterTest { analyticsService = analyticsService, clipboardHelper = clipboardHelper, appPreferencesStore = appPreferencesStore, + notificationCleaner = notificationCleaner, + sessionPreferencesStore = sessionPreferencesStore, ) } @@ -199,19 +203,14 @@ class RoomDetailsPresenterTest { givenRoomInfo( aRoomInfo( isEncrypted = true, - isDirect = true, + isDm = true, ) ) } val presenter = createRoomDetailsPresenter(room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { val initialState = awaitItem() - assertThat(initialState.roomType).isEqualTo( - RoomDetailsType.Dm( - me = myRoomMember, - otherMember = otherRoomMember, - ) - ) + assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherMember = otherRoomMember)) cancelAndIgnoreRemainingEvents() } } @@ -289,7 +288,7 @@ class RoomDetailsPresenterTest { givenRoomInfo( aRoomInfo( isEncrypted = true, - isDirect = true, + isDm = true, ) ) } @@ -312,7 +311,6 @@ class RoomDetailsPresenterTest { val myRoomMember = aRoomMember(A_SESSION_ID) val otherRoomMember = aRoomMember(A_USER_ID_2) val room = aJoinedRoom( - isDirect = true, topic = null, roomPermissions = roomPermissions(), userDisplayNameResult = { Result.success(A_USER_NAME) }, @@ -330,7 +328,7 @@ class RoomDetailsPresenterTest { givenRoomInfo( aRoomInfo( - isDirect = true, + isDm = true, activeMembersCount = 2, topic = null, ) @@ -570,17 +568,11 @@ class RoomDetailsPresenterTest { roomPermissions = roomPermissions(), joinRule = JoinRule.Knock, ) - val featureFlagService = FakeFeatureFlagService( - mapOf(FeatureFlags.Knock.key to false) - ) val presenter = createRoomDetailsPresenter( room = room, - featureFlagService = featureFlagService, ) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { skipItems(1) - assertThat(awaitItem().canShowKnockRequests).isFalse() - featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true) assertThat(awaitItem().canShowKnockRequests).isTrue() room.givenRoomInfo(aRoomInfo(joinRule = JoinRule.Invite)) assertThat(awaitItem().canShowKnockRequests).isFalse() @@ -593,8 +585,7 @@ class RoomDetailsPresenterTest { val room = aJoinedRoom( roomPermissions = roomPermissions(), ) - val featureFlagService = FakeFeatureFlagService() - val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService) + val presenter = createRoomDetailsPresenter(room = room) presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { skipItems(1) with(awaitItem()) { @@ -620,6 +611,87 @@ class RoomDetailsPresenterTest { } } + @Test + fun `present - mark as read`() = runTest { + val markAsReadResult = lambdaRecorder> { _ -> Result.success(Unit) } + val room = aJoinedRoom( + markAsReadResult = markAsReadResult, + ) + val clearMessagesForRoomResult = lambdaRecorder { _, _ -> Result.success(Unit) } + val notificationCleaner = FakeNotificationCleaner( + clearMessagesForRoomLambda = clearMessagesForRoomResult, + ) + val presenter = createRoomDetailsPresenter( + room = room, + notificationCleaner = notificationCleaner, + ) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + skipItems(1) + with(awaitItem()) { + eventSink(RoomDetailsEvent.MarkAsRead) + } + assertThat(room.baseRoom.setUnreadFlagCalls).containsExactly(false) + markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ)) + clearMessagesForRoomResult.assertions().isCalledOnce().with( + value(room.sessionId), + value(room.roomId), + ) + } + } + + @Test + fun `present - mark as read - private`() = runTest { + val markAsReadResult = lambdaRecorder> { _ -> Result.success(Unit) } + val room = aJoinedRoom( + markAsReadResult = markAsReadResult, + ) + val sessionPreferencesStore = InMemorySessionPreferencesStore( + isSendPublicReadReceiptsEnabled = false, + ) + val clearMessagesForRoomResult = lambdaRecorder { _, _ -> Result.success(Unit) } + val notificationCleaner = FakeNotificationCleaner( + clearMessagesForRoomLambda = clearMessagesForRoomResult, + ) + val presenter = createRoomDetailsPresenter( + room = room, + notificationCleaner = notificationCleaner, + sessionPreferencesStore = sessionPreferencesStore, + ) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + skipItems(1) + with(awaitItem()) { + eventSink(RoomDetailsEvent.MarkAsRead) + } + assertThat(room.baseRoom.setUnreadFlagCalls).containsExactly(false) + markAsReadResult.assertions().isCalledOnce().with(value(ReceiptType.READ_PRIVATE)) + clearMessagesForRoomResult.assertions().isCalledOnce().with( + value(room.sessionId), + value(room.roomId), + ) + } + } + + @Test + fun `present - mark as unread`() = runTest { + val room = aJoinedRoom() + val onDoneResult = lambdaRecorder { } + val navigator = FakeRoomDetailsNavigator( + onDoneResult = onDoneResult + ) + val presenter = createRoomDetailsPresenter( + room = room, + navigator = navigator, + ) + presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) { + skipItems(1) + with(awaitItem()) { + eventSink(RoomDetailsEvent.MarkAsUnread) + } + onDoneResult.assertions().isCalledOnce() + assertThat(room.baseRoom.setUnreadFlagCalls).containsExactly(true) + } + } + private fun roomPermissions( canInvite: Boolean = true, canKick: Boolean = true, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt index 588a10a218..767ddd5dd9 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt @@ -6,15 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomdetails.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.matrix.api.core.UserId @@ -32,323 +36,332 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class RoomDetailsViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class RoomDetailsViewTest : RobolectricTest() { @Test - fun `click on back invokes expected callback`() { + fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( goBack = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `click on share invokes expected callback`() { + fun `click on share invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( onShareRoom = callback, ) - rule.clickOn(CommonStrings.action_share) + clickOn(CommonStrings.action_share) } } @Config(qualifiers = "h1024dp") @Test - fun `click on room members invokes expected callback`() { + fun `click on room members invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openRoomMemberList = callback, ) - rule.clickOn(CommonStrings.common_people) + clickOn(CommonStrings.common_people) } } @Config(qualifiers = "h1024dp") @Test - fun `click on polls invokes expected callback`() { + fun `click on polls invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openPollHistory = callback, ) - rule.clickOn(R.string.screen_polls_history_title) + clickOn(R.string.screen_polls_history_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on media gallery invokes expected callback`() { + fun `click on media gallery invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openMediaGallery = callback, ) - rule.clickOn(R.string.screen_room_details_media_gallery_title) + clickOn(R.string.screen_room_details_media_gallery_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on notification invokes expected callback`() { + fun `click on notification invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( openRoomNotificationSettings = callback, ) - rule.clickOn(R.string.screen_room_details_notification_title) + clickOn(R.string.screen_room_details_notification_title) } } @Test - fun `click on invite invokes expected callback`() { + fun `click on invite invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), invitePeople = callback, ) - rule.clickOn(CommonStrings.action_invite) + clickOn(CommonStrings.action_invite) } } @Test - fun `click on call invokes expected callback`() { + fun `click on call invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(CallIntent.AUDIO) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, - roomType = RoomDetailsType.Dm( - aRoomMember(UserId("@me:local.org")), - aRoomMember(UserId("@other:local.org")) - ), + roomType = RoomDetailsType.Dm(aRoomMember(UserId("@other:local.org"))), ), onJoinCallClick = callback, ) - rule.clickOn(CommonStrings.action_call) + clickOn(CommonStrings.action_call) } } @Test - fun `click on video call invokes expected callback`() { + fun `click on video call invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(CallIntent.VIDEO) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), onJoinCallClick = callback, ) - rule.clickOn(CommonStrings.common_video) + clickOn(CommonStrings.common_video) } } @Config(qualifiers = "h1024dp") @Test - fun `click on pinned messages invokes expected callback`() { + fun `click on pinned messages invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canInvite = true, ), onPinnedMessagesClick = callback, ) - rule.clickOn(R.string.screen_room_details_pinned_events_row_title) + clickOn(R.string.screen_room_details_pinned_events_row_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on security and privacy invokes expected callback`() { + fun `click on security and privacy invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canShowSecurityAndPrivacy = true, ), onSecurityAndPrivacyClick = callback, ) - rule.clickOn(R.string.screen_room_details_security_and_privacy_title) + clickOn(R.string.screen_room_details_security_and_privacy_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on add topic emit expected event`() { + fun `click on add topic emit expected event`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(RoomDetailsAction.AddTopic) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), roomTopic = RoomTopicState.CanAddTopic, ), onActionClick = callback, ) - rule.clickOn(R.string.screen_room_details_add_topic_title) + clickOn(R.string.screen_room_details_add_topic_title) } } @Test - fun `click on menu edit emit expected event`() { + fun `click on menu edit emit expected event`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(RoomDetailsAction.Edit) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canEdit = true, ), onActionClick = callback, ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.clickOn(CommonStrings.action_edit) + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + clickOn(CommonStrings.action_edit) } } @Test - fun `click on avatar test`() { + fun `click on avatar test`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aRoomDetailsState( eventSink = eventsRecorder, roomAvatarUrl = "an_avatar_url", ) val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url") - rule.setRoomDetailView( + setRoomDetailView( state = state, openAvatarPreview = callback, ) - rule.onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() + onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() callback.assertSuccess() } @Test - fun `click on avatar test on DM`() { + fun `click on avatar test on DM`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) val state = aRoomDetailsState( - roomType = RoomDetailsType.Dm( - aRoomMember(), - aDmRoomMember(avatarUrl = "an_avatar_url"), - ), + roomType = RoomDetailsType.Dm(aDmRoomMember(avatarUrl = "an_avatar_url"),), roomName = "Daniel", eventSink = eventsRecorder, ) val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url") - rule.setRoomDetailView( + setRoomDetailView( state = state, openAvatarPreview = callback, ) - rule.onNodeWithTag(TestTags.memberDetailAvatar.value).performClick() + onNodeWithTag(TestTags.roomDetailAvatar.value).performClick() callback.assertSuccess() } @Test - fun `click on mute emit expected event`() { + fun `click on mute emit expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDetailsState( eventSink = eventsRecorder, roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES), ) - rule.setRoomDetailView( + setRoomDetailView( state = state, ) - rule.clickOn(CommonStrings.common_mute) + clickOn(CommonStrings.common_mute) eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification) } @Test - fun `click on unmute emit expected event`() { + fun `click on unmute emit expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDetailsState( eventSink = eventsRecorder, roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE), ) - rule.setRoomDetailView( + setRoomDetailView( state = state, ) - rule.clickOn(CommonStrings.common_unmute) + clickOn(CommonStrings.common_unmute) eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification) } @Config(qualifiers = "h1024dp") @Test - fun `click on favorite emit expected Event`() { + fun `click on favorite emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.common_favourite) + clickOn(CommonStrings.common_favourite) eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true)) } @Config(qualifiers = "h1500dp") @Test - fun `click on leave emit expected Event`() { + fun `click on leave emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_room_details_leave_room_title) + clickOn(R.string.screen_room_details_leave_room_title) eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom(needsConfirmation = true)) } @Config(qualifiers = "h1500dp") @Test - fun `click on report room invokes expected callback`() { + fun `click on report room invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), ), onReportRoomClick = callback, ) - rule.clickOn(CommonStrings.action_report_room) + clickOn(CommonStrings.action_report_room) } } @Config(qualifiers = "h1024dp") @Test - fun `click on knock requests invokes expected callback`() { + fun `click on knock requests invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), canShowKnockRequests = true, ), onKnockRequestsClick = callback, ) - rule.clickOn(R.string.screen_room_details_requests_to_join_title) + clickOn(R.string.screen_room_details_requests_to_join_title) } } @Config(qualifiers = "h1024dp") @Test - fun `click on profile invokes the expected callback`() { + fun `click on profile invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(A_USER_ID) { callback -> - rule.setRoomDetailView( + setRoomDetailView( state = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), - roomMemberDetailsState = aUserProfileState(userId = A_USER_ID), + roomType = RoomDetailsType.Dm(aDmRoomMember(userId = A_USER_ID)), + dmOtherMemberDetailsState = aUserProfileState(userId = A_USER_ID), ), onProfileClick = callback, ) - rule.clickOn(R.string.screen_room_details_profile_row_title) + clickOn(R.string.screen_room_details_profile_row_title) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `click on invite invokes the expected callback`() = runAndroidComposeUiTest { + ensureCalledOnce { callback -> + setRoomDetailView( + state = aRoomDetailsState( + eventSink = EventsRecorder(expectEvents = false), + roomType = RoomDetailsType.Dm( + aDmRoomMember(userId = UserId("@other:local.org")), + ), + dmOtherMemberDetailsState = aUserProfileState(userId = A_USER_ID), + canInvite = true, + ), + invitePeople = callback, + ) + onAllNodesWithText(activity!!.getString(R.string.screen_room_details_invite_title)).onLast().performClick() } } } -private fun AndroidComposeTestRule.setRoomDetailView( +private fun AndroidComposeUiTest.setRoomDetailView( state: RoomDetailsState = aRoomDetailsState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/roomdetailsedit/impl/src/main/res/values-ca/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..8be6967ace --- /dev/null +++ b/features/roomdetailsedit/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "Edita detalls" + "Un error desconegut ha impedit l\'intercanvi d\'informació." + "No s\'ha pogut actualitzar la sala" + "Actualitzant sala…" + diff --git a/features/roomdetailsedit/impl/src/main/res/values-fa/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-fa/translations.xml index bbbd63d171..a339ecd57f 100644 --- a/features/roomdetailsedit/impl/src/main/res/values-fa/translations.xml +++ b/features/roomdetailsedit/impl/src/main/res/values-fa/translations.xml @@ -1,6 +1,6 @@ - "ویرایش اتاق" + "ویرایش جزییات" "خطایی ناشناخته رخ داد و اطّلاعات قابل تغییر نبودند." "ناتوان در به‌روز رسانی اتاق" "به‌روز کردن اتاق…" diff --git a/features/roomdetailsedit/impl/src/main/res/values-pl/translations.xml b/features/roomdetailsedit/impl/src/main/res/values-pl/translations.xml index c676ff46ed..25be496204 100644 --- a/features/roomdetailsedit/impl/src/main/res/values-pl/translations.xml +++ b/features/roomdetailsedit/impl/src/main/res/values-pl/translations.xml @@ -1,6 +1,6 @@ - "Edytuj pokój" + "Edytuj szczegóły" "Wystąpił nieznany błąd i nie można było zmienić informacji." "Nie można zaktualizować pokoju" "Aktualizuję pokój…" diff --git a/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt b/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt index 71fb143074..dab0381232 100644 --- a/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt +++ b/features/roomdetailsedit/impl/src/test/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditViewTest.kt @@ -5,19 +5,21 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomdetailsedit.impl import androidx.activity.ComponentActivity import androidx.annotation.StringRes +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assert import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.isEditable -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 androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.testtags.TestTags @@ -27,59 +29,54 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Ignore -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomDetailsEditViewTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomDetailsEditViewTest : RobolectricTest() { @Test - fun `clicking on back emits the expected Event`() { + fun `clicking on back emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) } @Test - fun `clicking on discard when confirming exit emits the expected Event`() { + fun `clicking on discard when confirming exit emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_discard) + clickOn(CommonStrings.action_discard) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress) } @Test - fun `clicking on save when confirming exit emits the expected Event`() { + fun `clicking on save when confirming exit emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save, inDialog = true) + clickOn(CommonStrings.action_save, inDialog = true) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) } @Test - fun `when edition is successful, the expected callback is invoked`() { + fun `when edition is successful, the expected callback is invoked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveAction = AsyncAction.Success(Unit) @@ -90,55 +87,55 @@ class RoomDetailsEditViewTest { } @Test - fun `when name is changed, the expected Event is emitted`() { + fun `when name is changed, the expected Event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomRawName = "Marketing", ), ) - rule.onNodeWithText("Marketing").performTextInput("A") + onNodeWithText("Marketing").performTextInput("A") eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing")) } @Test - fun `when user cannot change name, nothing happen`() { + fun `when user cannot change name, nothing happen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomRawName = "Marketing", canChangeName = false, ), ) - rule.onNodeWithText("Marketing").assert(!isEditable()) + onNodeWithText("Marketing").assert(!isEditable()) } @Test - fun `when topic is changed, the expected Event is emitted`() { + fun `when topic is changed, the expected Event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomTopic = "My Topic", ), ) - rule.onNodeWithText("My Topic").performTextInput("A") + onNodeWithText("My Topic").performTextInput("A") eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic")) } @Test - fun `when user cannot change topic, nothing happen`() { + fun `when user cannot change topic, nothing happen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, roomTopic = "My Topic", canChangeTopic = false, ), ) - rule.onNodeWithText("My Topic").assert(!isEditable()) + onNodeWithText("My Topic").assert(!isEditable()) } @Ignore("This test is failing because the bottom sheet does not open") @@ -171,73 +168,73 @@ class RoomDetailsEditViewTest { private fun testAvatarChange( @StringRes stringActionRes: Int, expectedEvent: RoomDetailsEditEvent.HandleAvatarAction, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, ), ) // Open the bottom sheet - rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() - rule.onNodeWithText(rule.activity.getString(stringActionRes)).assertExists() - rule.clickOn(stringActionRes) + onNode(hasTestTag(TestTags.editAvatar.value)).performClick() + onNodeWithText(activity!!.getString(stringActionRes)).assertExists() + clickOn(stringActionRes) eventsRecorder.assertSingle(expectedEvent) } @Test - fun `when user cannot change avatar, nothing happen`() { + fun `when user cannot change avatar, nothing happen`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, canChangeAvatar = false, ), ) - rule.onNode(hasTestTag(TestTags.editAvatar.value)).performClick() - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_take_photo)).assertDoesNotExist() + onNode(hasTestTag(TestTags.editAvatar.value)).performClick() + onNodeWithText(activity!!.getString(CommonStrings.action_take_photo)).assertDoesNotExist() } @Test - fun `when save is clicked, the expected Event is emitted`() { + fun `when save is clicked, the expected Event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveButtonEnabled = true, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save) } @Test - fun `when save is clicked, but nothing need to be saved, nothing happens`() { + fun `when save is clicked, but nothing need to be saved, nothing happens`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveButtonEnabled = false, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) } @Test - fun `when error is shown, closing the dialog emit the expected Event`() { + fun `when error is shown, closing the dialog emit the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDetailsEditView( + setRoomDetailsEditView( aRoomDetailsEditState( eventSink = eventsRecorder, saveAction = AsyncAction.Failure(RuntimeException("Whelp")), ), ) - rule.clickOn(CommonStrings.action_ok) + clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog) } } -private fun AndroidComposeTestRule.setRoomDetailsEditView( +private fun AndroidComposeUiTest.setRoomDetailsEditView( state: RoomDetailsEditState, onDone: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/roomdirectory/impl/src/main/res/values-ca/translations.xml b/features/roomdirectory/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..fc664290c1 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,5 @@ + + + "No s\'ha pogut carregar" + "Directori de sales" + diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index a50ad6a22c..b3b935596f 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -6,47 +6,44 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roomdirectory.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag 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 androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.testtags.TestTags import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnceWithParam -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomDirectoryViewTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomDirectoryViewTest : RobolectricTest() { @Test - fun `typing text in search field emits the expected Event`() { + fun `typing text in search field emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomDirectoryView( + setRoomDirectoryView( state = aRoomDirectoryState( eventSink = eventsRecorder, ) ) - rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( + onNodeWithTag(TestTags.searchTextField.value).performTextInput( text = "Test" ) eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) } @Test - fun `clicking on room item then onResultClick lambda is called once`() { + fun `clicking on room item then onResultClick lambda is called once`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( roomDescriptions = aRoomDescriptionList(), @@ -54,27 +51,27 @@ class RoomDirectoryViewTest { ) val clickedRoom = state.roomDescriptions.first() ensureCalledOnceWithParam(clickedRoom) { callback -> - rule.setRoomDirectoryView( + setRoomDirectoryView( state = state, onResultClick = callback, ) - rule.onNodeWithText(clickedRoom.computedName).performClick() + onNodeWithText(clickedRoom.computedName).performClick() } } @Test - fun `composing load more indicator emits expected Event`() { + fun `composing load more indicator emits expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( displayLoadMoreIndicator = true, eventSink = eventsRecorder, ) - rule.setRoomDirectoryView(state = state) + setRoomDirectoryView(state = state) eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) } } -private fun AndroidComposeTestRule.setRoomDirectoryView( +private fun AndroidComposeUiTest.setRoomDirectoryView( state: RoomDirectoryState, onBackClick: () -> Unit = EnsureNeverCalled(), onResultClick: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(), diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt index 8fb1ae0c03..efc87d8a40 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -17,6 +17,7 @@ interface RoomMemberModerationRenderer { fun Render( state: RoomMemberModerationState, onSelectAction: (ModerationAction, MatrixUser) -> Unit, + onAvatarClick: ((MatrixUser) -> Unit)?, modifier: Modifier, ) } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt index 05bf00c7eb..0eeded6434 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/DefaultRoomMemberModerationRenderer.kt @@ -25,10 +25,11 @@ class DefaultRoomMemberModerationRenderer : RoomMemberModerationRenderer { override fun Render( state: RoomMemberModerationState, onSelectAction: (ModerationAction, MatrixUser) -> Unit, + onAvatarClick: ((MatrixUser) -> Unit)?, modifier: Modifier ) { if (state is InternalRoomMemberModerationState) { - RoomMemberModerationView(state, onSelectAction, modifier) + RoomMemberModerationView(modifier = modifier, state = state, onSelectAction = onSelectAction, onAvatarClick = onAvatarClick) } else { SideEffect { Timber.d("RoomMemberModerationRenderer: Render called with unsupported state: $state") diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt index 120a299a7d..2bb4db0c69 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt @@ -14,6 +14,7 @@ import io.element.android.features.roommembermoderation.api.ModerationActionStat import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.toImmutableList @@ -78,8 +79,8 @@ class InternalRoomMemberModerationStateProvider : PreviewParameterProvider Unit, + onAvatarClick: ((MatrixUser) -> Unit)?, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { @@ -72,6 +75,7 @@ fun RoomMemberModerationView( user = selectedUser, actions = state.actions, onSelectAction = onSelectAction, + onAvatarClick = onAvatarClick, onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, ) } @@ -213,6 +217,7 @@ private fun RoomMemberActionsBottomSheet( user: MatrixUser, actions: ImmutableList, onSelectAction: (ModerationAction, MatrixUser) -> Unit, + onAvatarClick: ((MatrixUser) -> Unit)? = null, onDismiss: () -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -239,6 +244,13 @@ private fun RoomMemberActionsBottomSheet( modifier = Modifier .padding(bottom = 24.dp) .align(Alignment.CenterHorizontally) + .clickable(enabled = user.avatarUrl != null && onAvatarClick != null) { + coroutineScope.launch { + bottomSheetState.hide() + onAvatarClick?.invoke(user) + onDismiss() + } + } ) val bestName = user.getBestName() Text( @@ -271,13 +283,12 @@ private fun RoomMemberActionsBottomSheet( when (val action = actionState.action) { is ModerationAction.DisplayProfile -> { ListItem( - style = ListItemStyle.Primary, headlineContent = { Text(stringResource(R.string.screen_bottom_sheet_manage_room_member_member_user_info)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())), onClick = { coroutineScope.launch { - onSelectAction(action, user) bottomSheetState.hide() + onSelectAction(action, user) } }, enabled = actionState.isEnabled @@ -334,16 +345,18 @@ private fun RoomMemberActionsBottomSheet( @PreviewsDayNight @Composable internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { + val isDoingAction = listOf(state.kickUserAsyncAction, state.banUserAsyncAction, state.unbanUserAsyncAction).any { it is AsyncAction.Loading } + val modifier = if (isDoingAction) { + Modifier.fillMaxWidth().heightIn(min = 64.dp) + } else { + Modifier.fillMaxSize() + } ElementPreview { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 64.dp) - ) { + Box(modifier) { RoomMemberModerationView( state = state, - onSelectAction = { _, _ -> - }, + onSelectAction = { _, _ -> }, + onAvatarClick = {}, ) } } diff --git a/features/roommembermoderation/impl/src/main/res/values-be/translations.xml b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml index b4b1f35f18..d6d5fc1b8e 100644 --- a/features/roommembermoderation/impl/src/main/res/values-be/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-be/translations.xml @@ -7,7 +7,7 @@ "Блакіроўка %1$s" "Яны змогуць зноў далучыцца да гэтага пакоя, калі іх запросяць." "Прагляд профілю" - "Выдаліць удзельніка з пакоя" + "Выдаліць удзельніка" "Выдаліць удзельніка і забараніць далучацца ў будучыні?" "Выдаленне %1$s…" "Разблакіраваць" diff --git a/features/roommembermoderation/impl/src/main/res/values-ca/translations.xml b/features/roommembermoderation/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..9495112c4e --- /dev/null +++ b/features/roommembermoderation/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,20 @@ + + + "Bandeja de la sala" + "Bandeja" + "No podrà tornar a unir-se a aquesta sala encara que se\'l convidi." + "Segur que vols bandejar aquest membre?" + "Bandejant %1$s" + "Elimina" + "Podran tornar a unir-se a aquesta sala si se\'ls convida." + "Segur que vols eliminar aquest membre?" + "Veure perfil" + "Elimina de la sala" + "Vols eliminar l\'usuari i prohibir-li l\'accés en el futur?" + "Eliminant %1$s…" + "Readmet usuari" + "Desbandeja" + "Podrà tornar a unir-se a través d\'una invitació" + "Segur que vols readmetre aquest membre?" + "Readmetent %1$s" + diff --git a/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml index f17660f418..6ddb482189 100644 --- a/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-fa/translations.xml @@ -9,7 +9,7 @@ "در صورت دعوت می‌تواند دوباره به اتاق بپیوندد." "مطمئنید می‌خواهید این عضو را بردارید؟" "دیدن نمایه" - "برداشتن از اتاق" + "حذف کاربر" "برداشتن عضو و تحریم پیوستن در آینده؟" "برداشتن %1$s…" "تحریم نکردن از اتاق" diff --git a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml index 20246af787..40ea1f099a 100644 --- a/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-pl/translations.xml @@ -4,12 +4,14 @@ "Zbanuj" "Nie będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." "Czy na pewno chcesz zbanować tego członka?" + "Nie będą mogli ponownie dołączyć do tej przestrzeni, nawet jeśli zostaną zaproszeni, zachowają jednak członkostwo w pokojach lub podprzestrzeniach." "Banowanie %1$s" "Usuń" "Będą mogli ponownie dołączyć do tego pokoju, jeśli zostaną zaproszeni." "Czy na pewno chcesz usunąć tego członka?" + "Będą mogli ponownie dołączyć do tej przestrzeni, jeśli zostaną zaproszeni, zachowując jednocześnie członkostwo w pokojach lub podprzestrzeniach." "Wyświetl profil" - "Usuń z pokoju" + "Usuń użytkownika" "Usunąć członka i zablokować możliwość dołączenia w przyszłości?" "Usuwanie %1$s…" "Odbanuj z pokoju" diff --git a/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml index cd6dd40e55..0ddc115f7d 100644 --- a/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-uk/translations.xml @@ -4,10 +4,12 @@ "Заблокувати" "Він не зможе приєднатися до цієї кімнати знову, якщо його запросять." "Ви точно хочете заблокувати цього користувача?" + "Вони не зможуть знову приєднатися до цього простору, навіть якщо їх запросять, але збережуть своє членство в будь-яких кімнатах або підпросторах." "Блокування %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 2c7e6584b8..bfd456f130 100644 --- a/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml +++ b/features/roommembermoderation/impl/src/main/res/values-zh/translations.xml @@ -13,7 +13,7 @@ "查看个人资料" "移除用户" "删除成员并禁止重新加入?" - "正在移除 %1$s……" + "正在移除 %1$s…" "解封用户" "解封" "如果他们受到邀请,则可以重新加入" diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt index 6508b28053..f99e517767 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -6,39 +6,37 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.roommembermoderation.impl 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.testtags.TestTags +import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.pressTag +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class RoomMemberModerationViewTest { - @get:Rule val rule = createAndroidComposeRule() +class RoomMemberModerationViewTest : RobolectricTest() { @Test - fun `clicking on display profile action calls onSelectAction`() { + fun `clicking on display profile action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.DisplayProfile, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -48,16 +46,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) + clickOn(R.string.screen_bottom_sheet_manage_room_member_member_user_info) + // Gives time for bottomsheet to hide + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on kick user action calls onSelectAction`() { + fun `clicking on kick user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.KickUser, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -67,18 +67,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) // Gives time for bottomsheet to hide - rule.mainClock.advanceTimeBy(1_000) + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on ban user action calls onSelectAction`() { + fun `clicking on ban user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.BanUser, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -88,18 +88,18 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) + clickOn(R.string.screen_bottom_sheet_manage_room_member_ban) // Gives time for bottomsheet to hide - rule.mainClock.advanceTimeBy(1_000) + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking on unban user action calls onSelectAction`() { + fun `clicking on unban user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.UnbanUser, user) { callback -> - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = user, actions = listOf( @@ -109,100 +109,100 @@ class RoomMemberModerationViewTest { ), onSelectAction = callback ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) + clickOn(R.string.screen_bottom_sheet_manage_room_member_unban) // Gives time for bottomsheet to hide - rule.mainClock.advanceTimeBy(1_000) + mainClock.advanceTimeBy(1_000) } } @Test - fun `clicking submit on kick confirmation dialog sends DoKickUser event`() { + fun `clicking submit on kick confirmation dialog sends DoKickUser event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), kickUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) } @Test - fun `clicking dismiss on kick confirmation dialog sends Reset event`() { + fun `clicking dismiss on kick confirmation dialog sends Reset event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), kickUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogNegative.value) + pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `clicking submit on ban confirmation dialog sends DoBanUser event`() { + fun `clicking submit on ban confirmation dialog sends DoBanUser event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), banUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) } @Test - fun `clicking dismiss on ban confirmation dialog sends Reset event`() { + fun `clicking dismiss on ban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), banUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogNegative.value) + pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() { + fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogPositive.value) + pressTag(TestTags.dialogPositive.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser("")) } @Test - fun `clicking dismiss on unban confirmation dialog sends Reset event`() { + fun `clicking dismiss on unban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), unbanUserAsyncAction = AsyncAction.ConfirmingNoParams, eventSink = eventsRecorder ), ) - rule.pressTag(TestTags.dialogNegative.value) + pressTag(TestTags.dialogNegative.value) eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) } @Test - fun `disabled actions are not clickable`() { + fun `disabled actions are not clickable`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setRoomMemberModerationView( + setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), actions = listOf( @@ -211,18 +211,20 @@ class RoomMemberModerationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) + clickOn(R.string.screen_bottom_sheet_manage_room_member_remove) } } -private fun AndroidComposeTestRule.setRoomMemberModerationView( +private fun AndroidComposeUiTest.setRoomMemberModerationView( state: InternalRoomMemberModerationState, onSelectAction: (ModerationAction, MatrixUser) -> Unit = EnsureNeverCalledWithTwoParams(), + onAvatarClick: ((MatrixUser) -> Unit)? = EnsureNeverCalledWithParam(), ) { setSafeContent { RoomMemberModerationView( state = state, onSelectAction = onSelectAction, + onAvatarClick = onAvatarClick, ) } } diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupSetupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupSetupEntryPoint.kt new file mode 100644 index 0000000000..f674dac690 --- /dev/null +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupSetupEntryPoint.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.securebackup.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs + +interface SecureBackupSetupEntryPoint : FeatureEntryPoint { + data class Inputs(val isChangeRecoveryKeyUserStory: Boolean) : NodeInputs + + fun createNode( + parentNode: Node, + buildContext: BuildContext, + inputs: Inputs, + ): Node +} diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index b6117271f7..54d87ef22e 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -28,13 +28,14 @@ setupDependencyInjection() dependencies { implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) - implementation(projects.libraries.oidc.api) + implementation(projects.libraries.oauth.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) api(libs.statemachine) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPoint.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPoint.kt new file mode 100644 index 0000000000..29465b1be1 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPoint.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.features.securebackup.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.features.securebackup.api.SecureBackupSetupEntryPoint +import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode +import io.element.android.libraries.architecture.createNode + +@ContributesBinding(AppScope::class) +class DefaultSecureBackupSetupEntryPoint : SecureBackupSetupEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + inputs: SecureBackupSetupEntryPoint.Inputs, + ): Node { + return parentNode.createNode( + buildContext = buildContext, + plugins = listOf( + SecureBackupSetupNode.Inputs( + isChangeRecoveryKeyUserStory = inputs.isChangeRecoveryKeyUserStory, + ) + ), + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index d9fd8a1785..035e47d1c7 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -21,11 +21,11 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.securebackup.api.SecureBackupEntryPoint +import io.element.android.features.securebackup.api.SecureBackupSetupEntryPoint import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode import io.element.android.features.securebackup.impl.root.SecureBackupRootNode -import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.appyx.canPop @@ -39,6 +39,7 @@ import kotlinx.parcelize.Parcelize class SecureBackupFlowNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val secureBackupSetupEntryPoint: SecureBackupSetupEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = when (plugins.filterIsInstance().first().initialElement) { @@ -97,16 +98,18 @@ class SecureBackupFlowNode( createNode(buildContext, listOf(callback)) } NavTarget.Setup -> { - val inputs = SecureBackupSetupNode.Inputs( - isChangeRecoveryKeyUserStory = false, + secureBackupSetupEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + inputs = SecureBackupSetupEntryPoint.Inputs(isChangeRecoveryKeyUserStory = false), ) - createNode(buildContext, listOf(inputs)) } NavTarget.Change -> { - val inputs = SecureBackupSetupNode.Inputs( - isChangeRecoveryKeyUserStory = true, + secureBackupSetupEntryPoint.createNode( + parentNode = this, + buildContext = buildContext, + inputs = SecureBackupSetupEntryPoint.Inputs(isChangeRecoveryKeyUserStory = true), ) - createNode(buildContext, listOf(inputs)) } NavTarget.Disable -> { createNode(buildContext) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt index 8b2bc4dc90..34fca33e96 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt @@ -9,22 +9,10 @@ package io.element.android.features.securebackup.impl.enter import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -34,13 +22,11 @@ import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.modifiers.bringIntoViewOnImeVisible 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.ui.strings.CommonStrings -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.milliseconds @Composable fun SecureBackupEnterRecoveryKeyView( @@ -71,29 +57,13 @@ fun SecureBackupEnterRecoveryKeyView( } } -@OptIn(ExperimentalLayoutApi::class) @Composable private fun Content( state: SecureBackupEnterRecoveryKeyState, ) { - val bringIntoViewRequester = remember { BringIntoViewRequester() } - var isFocused by remember { mutableStateOf(false) } - val isImeVisible = WindowInsets.isImeVisible - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(isImeVisible, isFocused) { - // When the keyboard is shown, we want to scroll the text field into view - if (isImeVisible && isFocused) { - coroutineScope.launch { - // Delay to ensure the keyboard is fully shown - delay(100.milliseconds) - bringIntoViewRequester.bringIntoView() - } - } - } RecoveryKeyView( modifier = Modifier - .onFocusChanged { isFocused = it.isFocused } - .bringIntoViewRequester(bringIntoViewRequester) + .bringIntoViewOnImeVisible() .padding(top = 52.dp, bottom = 32.dp), state = state.recoveryKeyViewState, onClick = null, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt index c4a007f1d5..7dd6bf632a 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt @@ -25,6 +25,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab @@ -36,7 +37,7 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope -import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityOAuthResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -53,6 +54,7 @@ class ResetIdentityFlowNode( private val resetIdentityFlowManager: ResetIdentityFlowManager, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, + private val sessionEnterpriseService: SessionEnterpriseService, ) : BaseFlowNode( backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), buildContext = buildContext, @@ -123,12 +125,13 @@ class ResetIdentityFlowNode( null -> { Timber.d("No reset handle return, the reset is done.") } - is IdentityOidcResetHandle -> { + is IdentityOAuthResetHandle -> { Timber.d("Launching reset confirmation in MAS") - activity.openUrlInChromeCustomTab(null, darkTheme, handle.url) - Timber.d("Starting resetOidc") - resetJob = launch { handle.resetOidc() } - resetJob?.invokeOnCompletion { Timber.d("resetOidc ended") } + val url = sessionEnterpriseService.tweakMasUrl(handle.url) + activity.openUrlInChromeCustomTab(null, darkTheme, url) + Timber.d("Starting resetOAuth") + resetJob = launch { handle.resetOAuth() } + resetJob?.invokeOnCompletion { Timber.d("resetOAuth ended") } } is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index 8d32433ac5..3bb77ac206 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -8,8 +8,6 @@ package io.element.android.features.securebackup.impl.reset.password -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -32,7 +30,7 @@ import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusN 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.PasswordVisibilityToggle import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextFieldValidity import io.element.android.libraries.ui.strings.CommonStrings @@ -92,14 +90,10 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool singleLine = true, visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { - val image = - if (showPassword) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() - val description = - if (showPassword) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) - - Box(Modifier.clickable { showPassword = !showPassword }) { - Icon(imageVector = image, description) - } + PasswordVisibilityToggle( + visible = showPassword, + onToggle = { showPassword = !showPassword }, + ) }, validity = if (hasError) TextFieldValidity.Invalid else TextFieldValidity.None, supportingText = if (hasError) { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt index 198891473c..8055709f20 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -9,7 +9,6 @@ package io.element.android.features.securebackup.impl.setup.views 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 @@ -50,6 +49,7 @@ 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.Icon +import io.element.android.libraries.designsystem.theme.components.PasswordVisibilityToggle import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.testtags.TestTags @@ -223,16 +223,10 @@ private fun RecoveryKeyFormContent( ), placeholder = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder), trailingIcon = { - val image = - if (state.displayTextFieldContents) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff() - val description = - if (state.displayTextFieldContents) stringResource(CommonStrings.a11y_hide_password) else stringResource(CommonStrings.a11y_show_password) - Box(Modifier.clickable { toggleRecoveryKeyVisibility(!state.displayTextFieldContents) }) { - Icon( - imageVector = image, - contentDescription = description, - ) - } + PasswordVisibilityToggle( + visible = state.displayTextFieldContents, + onToggle = { toggleRecoveryKeyVisibility(!state.displayTextFieldContents) }, + ) }, ) } diff --git a/features/securebackup/impl/src/main/res/values-ca/translations.xml b/features/securebackup/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..0ca99788ed --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,69 @@ + + + "Elimina l\'emmagatzematge de claus" + "Activa còpia de seguretat" + "Emmagatzema la teva identitat criptogràfica i les claus de missatge de forma segura al servidor. Això et permetrà veure l\'historial de missatges en qualsevol dispositiu nou. %1$s." + "Emmagatzematge de claus" + "L\'emmagatzematge de claus ha d\'estar activat per poder configurar la recuperació." + "Puja claus des d\'aquest dispositiu" + "Permet l\'emmagatzematge de claus" + "Canvia la clau de recuperació" + "Recupera la teva identitat criptogràfica i l\'historial de missatges amb una clau de recuperació si has perdut tots els teus dispositius existents." + "Introdueix clau de recuperació" + "L\'emmagatzematge de claus no està sincronitzat." + "Configura la recuperació" + "Obre %1$s a un dispositiu d\'escriptori" + "Torna a iniciar sessió" + "Quan se\'t demani verificar el dispositiu, selecciona %1$s" + "“Restableix-ho tot”" + "Segueix les instruccions per crear una nova clau de recuperació" + "Desa la nova clau de recuperació en un gestor de contrasenyes o en una nota xifrada" + "Restableix el xifrat del teu compte mitjançant un altre dispositiu" + "Continua el restabliment" + "Es conservaran les dades del compte, contactes, preferències i les llistes de xats." + "Perdràs tots els missatges que estiguin desats únicament al servidor." + "Hauràs de tornar a verificar tots els teus dispositius i contactes existents." + "Només restableix la teva identitat si no tens accés a cap altre dispositiu on tinguis la sessió iniciada i has perdut la clau de recuperació." + "Si no pots confirmar la teva identitat, hauràs de restablir-la." + "Desactiva" + "Perdràs els missatges xifrats si tanques sessió en tots els dispositius." + "Segur que vols desactivar la còpia de seguretat?" + "Si elimines l\'emmagatzematge de claus, s\'eliminarà la teva identitat criptogràfica i les claus de missatge del servidor i es desactivaran les funcions de seguretat següents:" + "No tindràs l\'historial de missatges xifrats als dispositius nous" + "Perdràs l\'accés als teus missatges xifrats si tanques la sessió d\'%1$s a tot arreu" + "Estàs segur que vols desactivar l\'emmagatzematge de claus i eliminar-lo?" + "Obté una nova clau de recuperació si has perdut la que tens. Després de canviar la clau de recuperació, l\'antiga deixarà de funcionar." + "Genera una nova clau de recuperació" + "No ho comparteixis amb ningú!" + "Clau de recuperació canviada" + "Vols canviar la clau de recuperació?" + "Crea nova clau de recuperació" + "Assegura\'t que ningú vegi aquesta pantalla!" + "Torna a provar l\'accés a l\'emmagatzematge de claus." + "Clau de recuperació incorrecta" + "Si tens una clau de seguretat o una frase de seguretat, també funcionaran." + "Introdueix…" + "Has perdut la clau de recuperació?" + "Clau de recuperació confirmada" + "Introdueix clau de recuperació" + "Clau de recuperació copiada" + "Generant…" + "Desa clau de recuperació" + "Anota aquesta clau de recuperació en un lloc segur, com ara un gestor de contrasenyes, una nota xifrada o una caixa forta física." + "Toca per copiar la clau de recuperació" + "Desa la clau de recuperació en un lloc segur" + "Després d\'aquest pas, no podràs accedir a la nova clau de recuperació." + "Has desat la clau de recuperació?" + "L\'emmagatzematge de claus està protegit amb una clau de recuperació. Si necessites una nova clau de recuperació després d\'haver establerta, pots tornar-la a crear seleccionant ‘Canvia clau de recuperació’." + "Genera clau de recuperació" + "No ho comparteixis amb ningú!" + "Configuració de recuperació correcta" + "Configura la recuperació" + "Sí, restableix ara" + "Aquest procés és irreversible." + "Segur que vols restablir la teva identitat?" + "S\'ha produït un error desconegut. Comprova la contrasenya del compte i torna-ho a provar." + "Introdueix…" + "Confirma que vols restablir la teva identitat." + "Introdueix la contrasenya del compte per continuar" + diff --git a/features/securebackup/impl/src/main/res/values-de/translations.xml b/features/securebackup/impl/src/main/res/values-de/translations.xml index 8363cb9754..d53086a6d1 100644 --- a/features/securebackup/impl/src/main/res/values-de/translations.xml +++ b/features/securebackup/impl/src/main/res/values-de/translations.xml @@ -2,9 +2,9 @@ "Backup deaktivieren" "Backup aktivieren" - "Speichere deine kryptographische Identität und die Nachrichtenschlüssel auf dem Server. Auf diese Weise kannst du deinen Nachrichtenverlauf auf neuen Geräten einsehen. %1$s." + "Dadurch kannst du deinen Chatverlauf auf allen neuen Geräten einsehen. Er ist außerdem für die Sicherung von Chats und deiner digitalen Identität erforderlich. %1$s." "Schlüsselspeicher" - "Der Schlüsselspeicher muss aktiviert sein, um Datenwiederherstellung zu ermöglichen." + "Die Schlüsselspeicherung muss aktiviert sein, damit deine Chats gesichert werden können." "Schlüssel von diesem Gerät hochladen" "Schlüsselspeicherung zulassen" "Wiederherstellungsschlüssel ändern" @@ -34,10 +34,10 @@ "Du musst alle deine bestehenden Geräte und Kontakte erneut verifizieren." "Setze deine digitale Identität nur dann zurück, wenn du keinen Zugriff auf ein anderes verifiziertes Gerät hast und deinen Wiederherstellungsschlüssel verloren hast." "Bestätigung nicht möglich? Setze deine digitale Identität zurück." - "Ausschalten" - "Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist." - "Bist du sicher, dass du das Backup deaktivieren willst?" - "Das Löschen des Schlüsselspeichers entfernt deine kryptografische Identität und deine Nachrichtenschlüssel vom Server. Die folgenden Sicherheitsfunktionen werden deaktiviert:" + "Löschen" + "Wenn du alle deine Geräte entfernst, gehen deine verschlüsselten Chatverläufe verloren und du musst deine digitale Identität zurücksetzen." + "Bist du sicher, dass du den Schlüsselspeicher löschen möchtest?" + "Durch das Löschen des Schlüsselspeichers werden deine digitale Identität und deine Nachrichtenschlüssel vom Server entfernt und die folgenden Sicherheitsfunktionen deaktiviert:" "Kein Nachrichtenverlauf für verschlüsselte Nachrichten auf neuen Geräten" "Kein Zugriff auf verschlüsselten Nachrichten, wenn du überall von %1$s abgemeldet bist" "Möchtest du die Speicherung der Schlüssel wirklich deaktivieren und entfernen?" diff --git a/features/securebackup/impl/src/main/res/values-el/translations.xml b/features/securebackup/impl/src/main/res/values-el/translations.xml index ca9adbc5ac..0f4584271c 100644 --- a/features/securebackup/impl/src/main/res/values-el/translations.xml +++ b/features/securebackup/impl/src/main/res/values-el/translations.xml @@ -12,6 +12,7 @@ "Εισαγωγή κλειδιού ανάκτησης" "Ο αποθηκευτικός χώρος κλειδιών σου δεν είναι συγχρονισμένος αυτήν τη στιγμή." "Λήψη κλειδιού ανάκτησης" + "Οι συνομιλίες σας αποθηκεύονται αυτόματα με κρυπτογράφηση από άκρο σε άκρο. Για να επαναφέρετε αυτό το αντίγραφο ασφαλείας και να διατηρήσετε την ψηφιακή σας ταυτότητα όταν χάσετε την πρόσβαση σε όλες τις συσκευές σας, θα χρειαστείτε το κλειδί ανάκτησης. " "Άνοιγμα %1$s σε συσκευή υπολογιστή" "Συνδέσου ξανά στο λογαριασμό σου" "Όταν σου ζητηθεί να επαληθεύσεις τη συσκευή σου, επέλεξε %1$s" diff --git a/features/securebackup/impl/src/main/res/values-et/translations.xml b/features/securebackup/impl/src/main/res/values-et/translations.xml index 987fd88190..cb110d6f15 100644 --- a/features/securebackup/impl/src/main/res/values-et/translations.xml +++ b/features/securebackup/impl/src/main/res/values-et/translations.xml @@ -2,16 +2,17 @@ "Lülita võtmete varundamine välja" "Lülita võtmete varundamine sisse" - "Salvesta oma krüptoidentiteet ja sõnumite krüptovõtmed turvaliselt serveris. See tagab, et sinu sõnumite ajalugu on alati loetav, ka kõikides uutes seadmetes. %1$s." + "Salvesta oma digitaalne identiteet ja sõnumite krüptovõtmed turvaliselt serveris. See tagab, et sinu sõnumite ajalugu on alati loetav, ka kõikides uutes seadmetes. %1$s." "Krüptovõtmete varundus" - "Taastamise seadistamiseks peab võtmehoidla olema sisselülitatud." + "Sinu vestluste varundamiseks peab võtmehoidla olema sisselülitatud." "Laadi siin seadmes leiduvad võtmed üles" "Luba krüptovõtmete salvestamine" "Muuda taastevõtit" - "Kui sa oled kaotanud ligipääsu kõikidele oma olemasolevatele seadmetele, siis sa saad taastevõtme abil taastada ligipääsu oma krüptoidentiteedile ja sõnumite ajaloole." + "Kui sa oled kaotanud ligipääsu kõikidele oma olemasolevatele seadmetele, siis sa saad taastevõtme abil taastada ligipääsu oma digitaalsele identiteedile ja sõnumite ajaloole." "Sisesta taastevõti" "Sinu krüptovõtmete varundus pole hetkel enam sünkroonis." - "Seadista andmete taastamine" + "Seadista taastevõti" + "Sinu vestlused on automaatselt varundatud kasutades läbivat krüptimist. Kui peaksid kaotama ligipääsu kõikidele oma seadmetele, siis selle varukoopia taastamiseks ja oma digitaalse identiteedi säilitamiseks, on vaja taastevõtit." "Ava %1$s töölauaga seadmes" "Logi uuesti sisse oma kasutajakontole" "Kui sul palutakse seadet verifitseerida, vali %1$s" @@ -19,16 +20,36 @@ "Uue taastevõtme loomiseks palun järgi juhendit" "Salvesta oma uus taastevõti kas salasõnahalduris, krüptitud failis või mõnel muul turvalisel viisil" "Lähtesta oma konto krüptimine mõnest muust oma seadmest" + "Sisestatud taastevõti ei vasta süsteemis leiduvale" + "Lõpeta seadistus" + "Sisesta oma taastevõti uuesti." + "Korda oma taastevõtit" + + "Vähemalt %1$s tähemärki. Ära kasuta oma konto salasõna." + + "Tugevus" + "Salafraasi tugevus: %1$s" + "Mõõdukas" + "Tugev" + "Väga tugev" + "Väga kehv" + "Kehv" + "Sisesta taastevõti, mille saad meelde jätta." + "Sisesta taastevõti" + "Laadin taastevõtme nõudeid" + "Taastevõtme muutmiseks ava menüü: Seadistused → Krüptimine → Varundamine" + "Võid oma taastevõtit kasutada uute seadmete verifitseerimiseks või krüptitud vestluste taastamiseks, kui kaotate juurdepääsu kõigile oma seadmetele." + "Sinu varundus on nüüd täielikult seadistatud" "Jätka lähtestamisega" "Sinu kasutajakonto andmed, kontaktid, eelistused ja vestluste loend säiluvad" "Sa kaotad seniste sõnumite ajaloo" "Sa pead kõik oma olemasolevad seadmed ja kontaktid uuesti verifitseerima" - "Lähtesta oma identiteet vaid siis, kui sul pole ligipääsu mitte ühelegi oma seadmele ja sa oled kaotanud oma taastevõtme." - "Kui sa ühtegi muud võimalust ei leia, siis lähtesta oma identiteet." - "Lülita välja" - "Kui sa logid välja kõikidest oma seadmetest, siis sa kaotad ligipääsu oma krüptitud sõnumitele." - "Kas sa oled kindel, et soovid varukoopiate tegemise välja lülitada?" - "Varunduse väljalülitamisel kustutatakse hetkel olemasolev sinu krüptovõtmete varukoopia ning lülituvad välja veel mõned turvafunktsionaalsused. Sellisel juhul sul:" + "Lähtesta oma digitaalne identiteet vaid siis, kui sul pole ligipääsu mitte ühelegi oma seadmele ja sa oled kaotanud oma taastevõtme." + "Kui sa ühtegi muud võimalust ei leia, siis lähtesta oma digitaalne identiteet." + "Kustuta" + "Kui sa eemaldad kõik oma seadmed, siis sa kaotad ligipääsu oma krüptitud sõnumitele ja pead oma digitaalse identiteedi lähtestama." + "Kas oled kindel, et soovid võtmehoidla kustutada?" + "Võtmehoidla kustutamine eemaldab sinu digitaalse identiteedi ja sõnumivõtmed serverist ning lülitab välja järgmised turvafunktsionaalsused:" "sul ei ole krüptitud sõnumite ajalugu uutes seadmetes" "sa kaotad ligipääsu oma krüptitud sõnumitele, kui sa logid kõikjal välja rakendusest %1$s" "Kas sa oled kindel, et soovid varunduse välja lülitada?" @@ -58,12 +79,12 @@ "Loo oma taastevõti" "Ära jaga seda kellegagi" "Andmete taastamise seadistamine õnnestus" - "Seadista andmete taastamine" + "Seadista taastevõti" "Jah, lähtesta nüüd" "See tegevus on tagasipöördumatu." - "Kas sa oled kindel, et soovid oma andmete krüptimist lähtestada?" + "Kas sa oled kindel, et soovid oma digitaalse identiteedi lähtestada?" "Tekkis teadmata viga. Palun kontrolli, kas sinu kasutajakonto salasõna on õige ja proovi uuesti." "Sisesta…" - "Palun kinnita, et soovid oma andmete krüptimist lähtestada." + "Palun kinnita, et soovid oma digitaalse identiteedi lähtestada." "Jätkamaks sisesta oma kasutajakonto salasõna" diff --git a/features/securebackup/impl/src/main/res/values-fa/translations.xml b/features/securebackup/impl/src/main/res/values-fa/translations.xml index c16e5bd1d8..bd7a43ffba 100644 --- a/features/securebackup/impl/src/main/res/values-fa/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fa/translations.xml @@ -9,7 +9,7 @@ "تغییر کلید بازیابی" "ورود کلید بازیابی" "ذخیره‌ساز کلیدتان از هم‌گام بودن در آمده." - "برپایی بازیابی" + "دریافت کلید بازیابی" "گشودن %1$s در افزارهٔ میزکار" "ورود دوباره به حسابتان" "گزینش %1$s هنگام درخواست تأیید افزاره‌تان" @@ -23,10 +23,10 @@ "لازم است دوباره همهٔ آشنایان و افزاره‌های موجودتان را تأیید کنید" "فقط اگر به افزاره‌ای وارد شده از پیش دسترسی ندارید و کلید بازیابیتان را گم کرده‌اید بازنشانی کنید." "نمی‌توانید تأیید کنید؟ لازم است هویتتان را بازنشانی کنید." - "خاموش کردن" - "اگر از سیستم همه دستگاه ها خارج شده باشید، پیام های رمزگذاری شده خود را از دست خواهید داد." - "مطمئنید که می‌خواهید پشتیبان گیری را خاموش کنید؟" - "حذف فضای ذخیره سازی کلید، هویت رمزنگاری و کلیدهای پیام شما را از کارساز حذف می کند و ویژگی های امنیتی زیر را خاموش می کند:" + "حذف" + "اگر از تمام دستگاه‌هایتان خارج شوید، تاریخچه چت رمزگذاری‌شده خود را از دست خواهید داد و باید هویت دیجیتال خود را مجدداً تنظیم کنید." + "آیا مطمئن هستید که می‌خواهید کلید ذخیره‌سازی را حذف کنید؟" + "حذف محل ذخیره‌سازی کلید، کلیدهای هویت دیجیتال و پیام شما را از سرور حذف کرده و ویژگی‌های امنیتی زیر را غیرفعال می‌کند:" "سابقه پیام رمزگذاری شده در دستگاه های جدید نخواهید داشت" "اگر از %1$s در همه جا خارج شده باشید، دسترسی به پیام های رمزگذاری شده خود را از دست خواهید داد" "مطمئنید که می‌خواهید فضای ذخیره سازی کلید را خاموش کرده و آن را حذف کنید؟" @@ -56,7 +56,7 @@ "تولید کلید بازیابیتان" "با کسی هم‌رسانیش نکنید!" "برپایی بازیابی موفّق بود" - "برپایی بازیابی" + "دریافت کلید بازیابی" "بله. اکنون بازنشانی شود" "این فرایند بازگشت‌ناپذیر است." "ورود…" diff --git a/features/securebackup/impl/src/main/res/values-fr/translations.xml b/features/securebackup/impl/src/main/res/values-fr/translations.xml index dcdac3d919..3b738bf61e 100644 --- a/features/securebackup/impl/src/main/res/values-fr/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fr/translations.xml @@ -20,6 +20,27 @@ "Suivez les instructions pour créer une nouvelle clé de récupération" "Enregistrez votre nouvelle clé dans un gestionnaire de mots de passe ou dans une note chiffrée" "Réinitialisez le chiffrement de votre compte en utilisant un autre appareil" + "La clé de récupération ne correspond pas" + "Terminer la configuration" + "Saisissez à nouveau votre clé de récupération." + "Confirmez votre clé de récupération" + + "%1$d caractère minimum. N’utilisez pas le mot de passe de votre compte" + "%1$d caractères minimum. N’utilisez pas le mot de passe de votre compte" + + "Force" + "Force du mot de passe : %1$s" + "Moyenne" + "Forte" + "Très forte" + "Très faible" + "Faible" + "Choisissez une clé de récupération que vous pouvez mémoriser." + "Saisissez une clé de récupération" + "Chargement des exigences relatives à la clé de récupération" + "Pour modifier votre clé de récupération, accédez à Paramètres → Chiffrement → Changer la clé de récupération" + "Vous pouvez utiliser votre clé de récupération pour vérifier de nouveaux appareils ou restaurer vos conversations chiffrées si vous perdez l’accès à tous vos appareils." + "Votre sauvegarde est maintenant entièrement configurée." "Continuer la réinitialisation" "Les détails de votre compte, vos contacts, vos préférences et votre liste de discussions seront conservés" "Vous perdrez l’historique de vos messages" 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 a5d910e76b..3d89c15c5c 100644 --- a/features/securebackup/impl/src/main/res/values-hu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hu/translations.xml @@ -20,6 +20,27 @@ "Kövesse az utasításokat egy új helyreállítási kulcs létrehozásához" "Mentse az új helyreállítási kulcsot egy jelszókezelőbe vagy egy titkosított jegyzetbe." "A fiók titkosításának visszaállítása egy másik eszköz használatával" + "A megadott helyreállítási kulcsok nem egyeznek" + "Beállítás befejezése" + "Írja be újra a helyreállítási kulcsot." + "Erősítse meg a helyreállítási kulcsát" + + "Minimum %1$d karakter. Ne használja a fiók jelszavát." + "Minimum %1$d karakter. Ne használja a fiók jelszavát." + + "Erősség" + "Jelszó erőssége: %1$s" + "Mérsékelt" + "Erős" + "Nagyon erős" + "Nagyon gyenge" + "Gyenge" + "Válasszon egy olyan helyreállítási kulcsot, amelyet könnyen megjegyezhet." + "Adja meg a helyreállítási kulcsot" + "Helyreállítási kulcs követelményeinek betöltése" + "A helyreállítási kulcs módosításához lépjen a Beállítások → Titkosítás → Biztonsági mentés menüpontra." + "A helyreállítási kulcs segítségével megerősítheti az új eszközöket, vagy visszaállíthatja a titkosított csevegéseket, ha elveszítette a hozzáférését az összes eszközéhez." + "A biztonsági mentés most már teljesen be van állítva" "Visszaállítás folytatása" "A fiókadatok, a kapcsolatok, a beállítások és a csevegéslista megmarad" "Elveszíti meglévő üzenetelőzményeit" diff --git a/features/securebackup/impl/src/main/res/values-ja/translations.xml b/features/securebackup/impl/src/main/res/values-ja/translations.xml index 7a7dd041fe..b9612c4e09 100644 --- a/features/securebackup/impl/src/main/res/values-ja/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ja/translations.xml @@ -6,7 +6,7 @@ "鍵の保管庫" "チャットをバックアップするには、鍵の保管庫を使用する必要があります。" "この端末上の鍵をアップロードします" - "鍵の保管庫を使用します" + "鍵の保管庫を使用" "回復鍵を変更" "あなたのチャットはエンドツーエンド暗号化を使用して自動的にバックアップされています。すべての端末を使用できない状況で、このバックアップからデジタルIDを復元するには、回復鍵が必要となります。" "回復鍵を入力" @@ -21,7 +21,7 @@ "生成された回復鍵をパスワードマネージャや暗号化に対応するメモアプリに保存してください。" "他の端末を使用して暗号化をリセット" "リセットを続行" - "アカウントの情報と連絡先や設定などは残ります" + "アカウントの情報や連絡先, 設定などは残ります" "サーバー上にのみ存在する過去のメッセージは確認できなくなります" "すべての端末と連絡先を再度検証する必要があります" "デジタルIDのリセットは、他のサインイン済みの端末と、回復鍵の両方へのアクセスを失った場合にのみ行ってください。" diff --git a/features/securebackup/impl/src/main/res/values-pl/translations.xml b/features/securebackup/impl/src/main/res/values-pl/translations.xml index fb30d786c0..fb1da3ca04 100644 --- a/features/securebackup/impl/src/main/res/values-pl/translations.xml +++ b/features/securebackup/impl/src/main/res/values-pl/translations.xml @@ -2,34 +2,57 @@ "Wyłącz backup" "Włącz backup" - "Bezpiecznie przechowuj swoją tożsamość kryptograficzną i klucze wiadomości na serwerze. Umożliwi to przeglądanie historii wiadomości na każdym nowym urządzeniu. %1$s" + "Umożliwi Ci to przeglądanie historii czatów na nowych urządzeniach i jest wymagane do tworzenia kopii zapasowych i tożsamości cyfrowej. %1$s." "Magazyn kluczy" - "Magazyn kluczy musi być włączony, aby włączyć przywracanie." + "Magazyn kluczy musi być włączony, aby włączyć archiwizowanie czatów." "Prześlij klucze z tego urządzenia" "Zezwól na magazynowanie kluczy" "Zmień klucz przywracania" - "Odzyskaj swoją tożsamość kryptograficzną i historię wiadomości za pomocą klucza przywracania, jeśli utraciłeś dostęp do wszystkich swoich urządzeń." + "Twoje czaty są automatycznie archiwizowane za pomocą szyfrowania end-to-end. Aby przywrócić tę kopię zapasową i swoją tożsamość cyfrową, wymagany będzie klucz przywracania." "Wprowadź klucz przywracania" "Magazyn kluczy nie jest zsynchronizowany." - "Skonfiguruj przywracanie" + "Uzyskaj klucz przywracania" + "Twoje czaty są automatycznie archiwizowane za pomocą szyfrowania end-to-end. Aby przywrócić tę kopię zapasową i swoją tożsamość cyfrową, wymagany będzie klucz przywracania." "Otwórz %1$s na urządzeniu stacjonarnym" "Zaloguj się ponownie na swoje konto" "Gdy pojawi się prośba o weryfikację urządzenia, wybierz %1$s" - "“Resetuj wszystko”" + "“Zresetuj wszystko”" "Postępuj zgodnie z instrukcjami, aby utworzyć nowy klucz przywracania" "Zapisz nowy klucz przywracania w menedżerze haseł lub notatce szyfrowanej" - "Resetuj szyfrowanie swojego konta za pomocą drugiego urządzenia" + "Zresetuj szyfrowanie swojego konta za pomocą drugiego urządzenia" + "Wprowadzony klucz przywracania nie pasuje" + "Zakończ konfigurację" + "Wprowadź ponownie klucz przywracania." + "Potwierdź swój klucz przywracania" + + "Co najmniej %1$d znak. Nie wykorzystuj swojego hasła." + "Co najmniej %1$d znaki. Nie wykorzystuj swojego hasła." + "Co najmniej %1$d znaków. Nie wykorzystuj swojego hasła." + + "Siła" + "Siła hasła: %1$s" + "Średnie" + "Silne" + "Bardzo silne" + "Bardzo słabe" + "Słabe" + "Wybierz klucz przywracania, który łatwo zapamiętasz." + "Wprowadź klucz przywracania" + "Wczytywanie wymagań hasła" + "Aby zmienić klucz przywracania przejdź do Ustawienia → Szyfrowanie → Kopia zapasowa" + "Możesz użyć klucza przywracania, żeby potwierdzić nowe urządzenia lub przywrócić zaszyfrowane czaty, jeśli stracisz dostęp do wszystkich swoich urządzeń." + "Twoja kopia zapasowa jest gotowa." "Kontynuuj resetowanie" "Szczegóły konta, kontakty, preferencje i lista czatów zostaną zachowane" "Utracisz istniejącą historię wiadomości" "Wymagana będzie ponowna weryfikacja istniejących urządzeń i kontaktów" - "Zresetuj swoją tożsamość tylko wtedy, gdy nie jesteś zalogowany na żadnym urządzeniu i straciłeś swój klucz przywracania." - "Zresetuj swoją tożsamość, jeśli nie możesz jej potwierdzić w inny sposób" - "Wyłącz" - "Jeśli wylogujesz się ze wszystkich urządzeń, stracisz wszystkie wiadomości szyfrowane." - "Czy na pewno chcesz wyłączyć backup?" - "Wyłączenie backupu spowoduje usunięcie kopii klucza szyfrowania i wyłączenie innych funkcji bezpieczeństwa. W takim przypadku będziesz:" - "Posiadał historii wiadomości szyfrowanych na nowych urządzeniach" + "Zresetuj swoją tożsamość tylko wtedy, gdy nie posiadasz dostępu do żadnego innego zweryfikowanego urządzenia i straciłeś swój klucz przywracania." + "Nie możesz potwierdzić? Zresetuj swoją tożsamość cyfrową." + "Usuń" + "Jeśli usuniesz wszystkie swoje urządzenia, stracisz zaszyfrowaną historię wiadomości i będziesz musiał zresetować swoją tożsamość cyfrową." + "Czy na pewno chcesz usunąć magazyn kluczy?" + "Usunięcie magazynu kluczy usunie Twoją tożsamość cyfrową i klucze wiadomości z serwera, wyłączając następujące funkcje bezpieczeństwa:" + "Stracisz dostęp do zaszyfrowanej historii wiadomości na nowych urządzeniach" "Utracisz dostęp do wiadomości szyfrowanych, jeśli zostaniesz wszędzie wylogowany z %1$s" "Czy na pewno chcesz wyłączyć backup?" "Uzyskaj nowy klucz przywracania, jeśli straciłeś dostęp do obecnego. Po zmianie klucza przywracania stary nie będzie już działał." @@ -58,12 +81,12 @@ "Wygeneruj klucz przywracania" "Nie udostępniaj tego nikomu!" "Skonfigurowano przywracanie pomyślnie" - "Skonfiguruj przywracanie" + "Uzyskaj klucz przywracania" "Tak, zresetuj teraz" "Tego procesu nie można odwrócić." - "Czy na pewno chcesz zresetować szyfrowanie?" + "Czy na pewno chcesz zresetować swoją tożsamość cyfrową?" "Wystąpił nieznany błąd. Sprawdź, czy hasło jest poprawne i spróbuj ponownie." "Wprowadź…" - "Potwierdź, że chcesz zresetować szyfrowanie." + "Potwierdź, że chcesz zresetować swoją tożsamość cyfrową." "Wprowadź hasło, aby kontynuować" diff --git a/features/securebackup/impl/src/main/res/values-pt/translations.xml b/features/securebackup/impl/src/main/res/values-pt/translations.xml index 1e5d8b03ab..66051ca04b 100644 --- a/features/securebackup/impl/src/main/res/values-pt/translations.xml +++ b/features/securebackup/impl/src/main/res/values-pt/translations.xml @@ -2,7 +2,7 @@ "Desativar a cópia de segurança" "Ativar a cópia de segurança" - "Guarda a tua identidade criptográfica e as chaves de mensagens de forma segura no servidor. Isto permitir-te-á ver o teu histórico de mensagens em qualquer dispositivo novo. %1$s." + "Permite-te ver o teu histórico de mensagens em qualquer dispositivo novo. É necessário para teres cópias de segurança das tuas conversas e da tua identidade digital. %1$s." "Armazenamento de chaves" "O armazenamento de chaves deve ser ativado para configurar a recuperação." "Carrega chaves a partir deste dispositivo" @@ -11,7 +11,7 @@ "Recupera a tua identidade criptográfica e o histórico de mensagens com uma chave de recuperação, caso tenhas perdido todos os teus dispositivos existentes." "Insere a chave de recuperação" "O teu armazenamento de chaves está atualmente dessincronizado." - "Configurar recuperação" + "Chave de recuperação" "Abre a %1$s num computador" "Iniciar sessão novamente" "Quando te for pedido para verificares o teu dispositivo, seleciona %1$s" @@ -25,10 +25,10 @@ "Necessitarás de verificar todos os teus dispositivos e contactos novamente." "Repõe a tua identidade apenas se não tiveres acesso a mais nenhum dispositivo com sessão iniciada e se tiveres perdido a tua chave de recuperação." "Repõe a tua identidade caso não consigas confirmar de outra forma" - "Desligar" - "Perderás as tuas mensagens cifradas se tiveres terminado a sessão em todos os teus dispositivos." - "Tens a certeza que queres desativar a cópia de segurança?" - "Desativar a cópia de segurança irá remover a atual cópia da chave de cifragem e desativar outras funcionalidades de segurança. Neste caso, irás:" + "Eliminar" + "Perderás os históricos de conversas cifradas e terás que repor a tua identidade digital caso removas todos os teus dispositivos." + "Tens a certeza que queres eliminar o armazenamento de chaves?" + "Eliminar o armazenamento de chaves irá remover a tua identidade digital e chaves de mensagem do servidor. Irá também desativar as seguintes funcionalidades de segurança:" "Não ter o histórico de mensagens cifradas em novos dispositivos" "Perder o acesso às tuas mensagens cifradas se terminares todas as sessões %1$s" "Tens a certeza que queres desativar a cópia de segurança?" @@ -58,7 +58,7 @@ "Gerar a tua chave de recuperação" "Não partilhes isto com ninguém!" "Recuperação configurada com sucesso" - "Configurar recuperação" + "Chave de recuperação" "Sim, repor agora" "Este processo é irreversível." "Tens a certeza que pretendes repor a tua cifra?" diff --git a/features/securebackup/impl/src/main/res/values-ro/translations.xml b/features/securebackup/impl/src/main/res/values-ro/translations.xml index 452a0543c8..2351a54923 100644 --- a/features/securebackup/impl/src/main/res/values-ro/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ro/translations.xml @@ -12,6 +12,7 @@ "Introduceți cheia de recuperare" "Backup-ul pentru chat nu este sincronizat în prezent." "Obțineți cheia de recuperare" + "Chaturile dumneavoastră sunt salvate automat cu criptare end-to-end. Pentru a restaura această copie de rezervă și a vă păstra identitatea digitală atunci când pierdeți accesul la toate dispozitivele dumneavoastră, veți avea nevoie de cheia de recuperare." "Deschideți %1$s pe un dispozitiv desktop" "Conectați-vă din nou la contul dumneavoastră" "Când vi se cere să vă verificați dispozitivul, selectați%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 ef80347ea4..1417de5766 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -8,13 +8,13 @@ "Загрузить ключи с этого устройства" "Разрешить хранение ключей" "Изменить ключ восстановления" - "Если вы потеряли доступ к другим устройствам, то сможете восстановить свою идентификацию и историю сообщений с помощью ключа восстановления." + "Ваши чаты автоматически резервируются с помощью сквозного шифрования. Для восстановления резервной копии и сохранения Вашей цифровой личности в случае потери доступа ко всем устройствам потребуется ключ восстановления." "Введите ключ восстановления" "В настоящее время резервная копия ваших чатов не синхронизирована." "Получить ключ восстановления" "Ваши чаты автоматически резервируются с использованием сквозного шифрования. Для восстановления этой резервной копии и сохранения вашей цифровой личности в случае потери доступа ко всем вашим устройствам вам потребуется ключ восстановления." "Откройте %1$s на компьютере" - "Войдите в свой аккаунт еще раз" + "Войдите в свой аккаунт ещё раз" "Когда потребуется подтвердить устройство, выберите %1$s" "«Сбросить все»" "Следуйте инструкциям, чтобы создать новый ключ восстановления" @@ -24,7 +24,7 @@ "Данные вашего аккаунта, контакты, настройки и список чатов будут сохранены" "Вы потеряете историю тех сообщений, которые хранятся только на сервере" "Вам нужно будет заново подтвердить все существующие устройства и контакты." - "Сбрасывайте ключ шифрования только в том случае, если у вас нет доступа к другому устройству, на котором выполнен вход, и вы потеряли ключ восстановления." + "Сбрасывайте ключ шифрования только в том случае, если у Вас нет доступа к другому устройству, на котором выполнен вход, и Вы потеряли ключ восстановления." "Не можете подтвердить? Вам потребуется сбросить свою цифровую идентификацию." "Удалить" "Вы потеряете зашифрованные сообщения, если выйдете из всех устройств." @@ -40,9 +40,9 @@ "Изменить ключ восстановления?" "Создать новый ключ восстановления" "Убедитесь, что никто не подсматривает)" - "Пожалуйста, попробуйте еще раз, чтобы подтвердить доступ к резервной копии чата." + "Пожалуйста, попробуйте ещё раз, чтобы подтвердить доступ к резервной копии чата." "Неверный ключ восстановления" - "Если у вас есть пароль для восстановления или секретный пароль/ключ, это тоже сработает." + "Если у Вас есть ключ/фраза для восстановления, это тоже сработает." "Введите…" "Потеряли ключ восстановления?" "Ключ восстановления подтвержден" diff --git a/features/securebackup/impl/src/main/res/values-sk/translations.xml b/features/securebackup/impl/src/main/res/values-sk/translations.xml index b02d5ecfbb..4e646ef47e 100644 --- a/features/securebackup/impl/src/main/res/values-sk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sk/translations.xml @@ -11,7 +11,7 @@ "Obnovte svoju kryptografickú totožnosť a históriu správ pomocou kľúča na obnovenie, ak ste stratili všetky svoje existujúce zariadenia." "Zadajte kľúč na obnovenie" "Vaše úložisko kľúčov nie je momentálne synchronizované." - "Nastaviť obnovenie" + "Získať kľúč na obnovenie" "Otvoriť %1$s v stolnom počítači" "Znova sa prihláste do svojho účtu" "Keď sa zobrazí výzva na overenie vášho zariadenia, vyberte %1$s" @@ -25,10 +25,10 @@ "Budete musieť znova overiť všetky existujúce zariadenia a kontakty" "Obnovte svoju totožnosť iba vtedy, ak nemáte prístup k inému prihlásenému zariadeniu a stratili ste kľúč na obnovenie." "Znovu nastavte svoju totožnosť v prípade, že ju nemôžete potvrdiť iným spôsobom" - "Vypnúť" - "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých zariadení" - "Ste si istí, že chcete vypnúť zálohovanie?" - "Vypnutím zálohovania sa odstráni aktuálna záloha šifrovacích kľúčov a vypnú sa ďalšie bezpečnostné funkcie. V tomto prípade:" + "Odstrániť" + "Ak odstránite všetky svoje zariadenia, stratíte svoju zašifrovanú históriu správ a budete si musieť obnoviť svoju digitálnu identitu." + "Naozaj chcete odstrániť úložisko kľúčov?" + "Odstránením úložiska kľúčov sa zo servera odstránia vaše kľúče digitálnej identity a správ a vypnú sa nasledujúce bezpečnostné funkcie:" "Na nových zariadeniach nebudete mať zašifrovanú históriu správ" "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite z aplikácie %1$s na všetkých zariadeniach" "Ste si istí, že chcete vypnúť zálohovanie?" @@ -58,7 +58,7 @@ "Vygenerujte si váš kľúč na obnovenie" "Nezdieľajte to s nikým!" "Úspešné nastavenie obnovy" - "Nastaviť obnovenie" + "Získať kľúč na obnovenie" "Áno, znovu nastaviť teraz" "Tento proces je nezvratný." "Naozaj chcete obnoviť svoje šifrovanie?" diff --git a/features/securebackup/impl/src/main/res/values-uk/translations.xml b/features/securebackup/impl/src/main/res/values-uk/translations.xml index 218e6d64fd..82a96fcab9 100644 --- a/features/securebackup/impl/src/main/res/values-uk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-uk/translations.xml @@ -12,6 +12,7 @@ "Введіть ключ відновлення" "Сховище ключів наразі не синхронізовано." "Налаштувати відновлення" + "Ваші чати автоматично резервуються з використанням наскрізного шифрування. Щоб відновити цю резервну копію та зберегти свою цифрову ідентичність у разі втрати доступу до всіх своїх пристроїв, вам знадобиться ключ відновлення." "Відкрийте %1$s на комп\'ютері" "Увійдіть до вашого облікового запису знову" "Коли вас попросять підтвердити пристрій, виберіть %1$s" 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 c2e7d5c2cb..2b59a54ada 100644 --- a/features/securebackup/impl/src/main/res/values-zh/translations.xml +++ b/features/securebackup/impl/src/main/res/values-zh/translations.xml @@ -20,6 +20,26 @@ "按照说明创建新的恢复密钥" "将新的恢复密钥保存在密码管理器或加密备忘录中" "使用其它设备重置账户的加密" + "输入的恢复密钥不匹配" + "完成设置" + "再次输入恢复密钥。" + "确认恢复密钥" + + "最少 %1$d 个字符。请勿使此处的内容与账户密码相同。" + + "强度" + "口令强度:%1$s" + "中等" + "强" + "非常强" + "非常弱" + "弱" + "选择你能记住的恢复密钥。" + "输入恢复密钥" + "正在加载恢复密钥要求" + "转到“设置”→“加密”→“备份”以更改恢复密钥。" + "如果你无法访问所有设备,可以使用恢复密钥以确认新设备或恢复加密聊天。" + "你已完成备份设置" "继续重置" "你的账户信息、联系人、偏好和聊天列表将被保留" "你将丢失所有仅存储在服务器上的消息历史" @@ -48,7 +68,7 @@ "恢复密钥已确认" "输入恢复密钥" "恢复密钥已复制" - "正在生成……" + "正在生成…" "保存恢复密钥" "将此恢复密钥保存在安全的地方,例如密码管理器、加密笔记或物理保险箱。" "点击复制恢复密钥" diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index e0fc2d37e7..5acae5f07f 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -20,6 +20,27 @@ "Follow the instructions to create a new recovery key" "Save your new recovery key in a password manager or encrypted note" "Reset the encryption for your account using another device" + "The recovery key you entered doesn\'t match" + "Finish setup" + "Enter your recovery key again." + "Confirm your recovery key" + + "Minimum %1$d character. Do not use your account password" + "Minimum %1$d characters. Do not use your account password" + + "Strength" + "Passphrase strength: %1$s" + "Moderate" + "Strong" + "Very strong" + "Very weak" + "Weak" + "Choose a recovery key that you can memorize." + "Enter a recovery key" + "Loading recovery key requirements" + "To change your recovery key, go to Settings → Encryption → Backup" + "You can use your recovery key to confirm new devices or restore your encrypted chats if you lose access to all your devices." + "Your backup is now fully set up" "Continue reset" "Your account details, contacts, preferences, and chat list will be kept" "You will lose any message history that’s stored only on the server" diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.kt index 9e984f1ec0..6c2cb9984f 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPointTest.kt @@ -32,6 +32,7 @@ class DefaultSecureBackupEntryPointTest { SecureBackupFlowNode( buildContext = buildContext, plugins = plugins, + secureBackupSetupEntryPoint = DefaultSecureBackupSetupEntryPoint(), ) } val callback = object : SecureBackupEntryPoint.Callback { diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt new file mode 100644 index 0000000000..e37dab748f --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupSetupEntryPointTest.kt @@ -0,0 +1,57 @@ +/* + * 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.securebackup.impl + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.google.common.truth.Truth.assertThat +import io.element.android.features.securebackup.api.SecureBackupSetupEntryPoint +import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode +import io.element.android.features.securebackup.impl.setup.SecureBackupSetupPresenter +import io.element.android.features.securebackup.impl.setup.SecureBackupSetupStateMachine +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.node.TestParentNode +import org.junit.Rule +import org.junit.Test + +class DefaultSecureBackupSetupEntryPointTest { + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + @Test + fun `test node builder`() { + val entryPoint = DefaultSecureBackupSetupEntryPoint() + val parentNode = TestParentNode.create { buildContext, plugins -> + SecureBackupSetupNode( + buildContext = buildContext, + plugins = plugins, + presenterFactory = object : SecureBackupSetupPresenter.Factory { + override fun create(isChangeRecoveryKeyUserStory: Boolean) = SecureBackupSetupPresenter( + isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory, + stateMachine = SecureBackupSetupStateMachine(), + encryptionService = FakeEncryptionService(), + ) + }, + snackbarDispatcher = SnackbarDispatcher(), + ) + } + val inputs = SecureBackupSetupEntryPoint.Inputs(isChangeRecoveryKeyUserStory = true) + val result = entryPoint.createNode( + parentNode = parentNode, + buildContext = BuildContext.root(null), + inputs = inputs, + ) + assertThat(result).isInstanceOf(SecureBackupSetupNode::class.java) + assertThat(result.plugins).contains(SecureBackupSetupNode.Inputs(isChangeRecoveryKeyUserStory = true)) + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt index d9324fdb91..fcf050f925 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt @@ -6,17 +6,19 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securebackup.impl.enter import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings @@ -26,58 +28,53 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class SecureBackupEnterRecoveryKeyViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class SecureBackupEnterRecoveryKeyViewTest : RobolectricTest() { @Test - fun `back key pressed - calls onBackClick`() { + fun `back key pressed - calls onBackClick`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(), onBackClick = callback, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `back button clicked - calls onBackClick`() { + fun `back button clicked - calls onBackClick`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(), onBackClick = callback, ) - rule.pressBack() + pressBack() } } @Test @Config(qualifiers = "h1024dp") - fun `tapping on Continue when key is valid - calls expected action`() { + fun `tapping on Continue when key is valid - calls expected action`() = runAndroidComposeUiTest { val recorder = EventsRecorder() - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) } @Test - fun `entering a char emits the expected event`() { + fun `entering a char emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - rule.onNodeWithText(keyValue).performTextInput("X") + onNodeWithText(keyValue).performTextInput("X") recorder.assertSingle( SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("X$keyValue") ) @@ -85,43 +82,43 @@ class SecureBackupEnterRecoveryKeyViewTest { @Test @Config(qualifiers = "h1024dp") - fun `toggling the visibility of the textfield changes it`() { + fun `toggling the visibility of the textfield changes it`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - rule.setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) + setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) // Initially, the text field should be visible - rule.onNodeWithText(keyValue).assertExists() + onNodeWithText(keyValue).assertExists() - rule.onNodeWithContentDescription(rule.activity.getString(CommonStrings.a11y_hide_password)).performClick() + onNodeWithContentDescription(activity!!.getString(CommonStrings.a11y_hide_password)).performClick() - rule.waitForIdle() + waitForIdle() recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false)) } @Test - fun `validating from keyboard emits the expected event`() { + fun `validating from keyboard emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) - rule.onNodeWithText(keyValue).performImeAction() + onNodeWithText(keyValue).performImeAction() recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) } @Test - fun `when submit action succeeds - calls onDone`() { + fun `when submit action succeeds - calls onDone`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setSecureBackupEnterRecoveryKeyView( + setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(submitAction = AsyncAction.Success(Unit)), onDone = callback, ) } } - private fun AndroidComposeTestRule.setSecureBackupEnterRecoveryKeyView( + private fun AndroidComposeUiTest.setSecureBackupEnterRecoveryKeyView( state: SecureBackupEnterRecoveryKeyState, onDone: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt index 6cfd061103..8173c20b3d 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securebackup.impl.reset.password import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -22,64 +24,58 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ResetIdentityPasswordViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class ResetIdentityPasswordViewTest : RobolectricTest() { @Test - fun `pressing the back HW button invokes the expected callback`() { + fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), onBack = it, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `clicking on the back navigation button invokes the expected callback`() { + fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = {}), onBack = it, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking 'Reset identity' confirms the reset`() { + fun `clicking 'Reset identity' confirms the reset`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Uninitialized, eventSink = eventsRecorder), ) - rule.onNodeWithText("Password").performTextInput("A password") + onNodeWithText("Password").performTextInput("A password") - rule.clickOn(CommonStrings.action_reset_identity) + clickOn(CommonStrings.action_reset_identity) eventsRecorder.assertSingle(ResetIdentityPasswordEvent.Reset("A password")) } @Test - fun `modifying the password dismisses the error state`() { + fun `modifying the password dismisses the error state`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetPasswordView( + setResetPasswordView( ResetIdentityPasswordState(resetAction = AsyncAction.Failure(IllegalStateException("A failure")), eventSink = eventsRecorder), ) - rule.onNodeWithText("Password").performTextInput("A password") + onNodeWithText("Password").performTextInput("A password") eventsRecorder.assertSingle(ResetIdentityPasswordEvent.DismissError) } } -private fun AndroidComposeTestRule.setResetPasswordView( +private fun AndroidComposeUiTest.setResetPasswordView( state: ResetIdentityPasswordState, onBack: () -> Unit = EnsureNeverCalled(), ) { diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt index a913a9af27..0d95bf709f 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securebackup.impl.reset.root 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.securebackup.impl.R import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -20,76 +22,70 @@ import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class ResetIdentityRootViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class ResetIdentityRootViewTest : RobolectricTest() { @Test - fun `pressing the back HW button invokes the expected callback`() { + fun `pressing the back HW button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), onBack = it, ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `clicking on the back navigation button invokes the expected callback`() { + fun `clicking on the back navigation button invokes the expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = {}), onBack = it, ) - rule.pressBack() + pressBack() } } @Test @Config(qualifiers = "h720dp") - fun `clicking Continue displays the confirmation dialog`() { + fun `clicking Continue displays the confirmation dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), ) - rule.clickOn(R.string.screen_encryption_reset_action_continue_reset) + clickOn(R.string.screen_encryption_reset_action_continue_reset) eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) } @Test - fun `clicking 'Yes, reset now' confirms the reset`() { + fun `clicking 'Yes, reset now' confirms the reset`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = true, eventSink = {}), onContinue = it, ) - rule.clickOn(R.string.screen_reset_encryption_confirmation_alert_action) + clickOn(R.string.screen_reset_encryption_confirmation_alert_action) } } @Test - fun `clicking Cancel dismisses the dialog`() { + fun `clicking Cancel dismisses the dialog`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setResetRootView( + setResetRootView( ResetIdentityRootState(displayConfirmationDialog = true, eventSink = eventsRecorder), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(ResetIdentityRootEvent.DismissDialog) } } -private fun AndroidComposeTestRule.setResetRootView( +private fun AndroidComposeUiTest.setResetRootView( state: ResetIdentityRootState, onBack: () -> Unit = EnsureNeverCalled(), onContinue: () -> Unit = EnsureNeverCalled(), diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt index a3cf920d82..4aede3c534 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt @@ -117,7 +117,7 @@ class SecureBackupSetupPresenterTest { @Test fun `present - handle errors`() = runTest { val encryptionService = FakeEncryptionService( - enableRecoveryLambda = { Result.failure(IllegalStateException("Test error")) } + enableRecoveryLambda = { _, _ -> Result.failure(IllegalStateException("Test error")) } ) val presenter = createSecureBackupSetupPresenter( isChangeRecoveryKeyUserStory = false, @@ -189,7 +189,7 @@ class SecureBackupSetupPresenterTest { private fun createSecureBackupSetupPresenter( isChangeRecoveryKeyUserStory: Boolean = false, encryptionService: EncryptionService = FakeEncryptionService( - enableRecoveryLambda = { Result.success(Unit) }, + enableRecoveryLambda = { _, _ -> Result.success("") }, ), ): SecureBackupSetupPresenter { return SecureBackupSetupPresenter( diff --git a/features/securityandprivacy/impl/src/main/res/values-be/translations.xml b/features/securityandprivacy/impl/src/main/res/values-be/translations.xml index 45ca9760ac..b2e90afeee 100644 --- a/features/securityandprivacy/impl/src/main/res/values-be/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-be/translations.xml @@ -1,4 +1,6 @@ + "Рэдагаваць адрас" "Вашы прасторы" + "Адрас" diff --git a/features/securityandprivacy/impl/src/main/res/values-ca/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..4d541af9d1 --- /dev/null +++ b/features/securityandprivacy/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,34 @@ + + + "És necessària una adreça perquè sigui visible al directori públic." + "Edita adreça" + "Afegeix adreça" + "Tothom ha de sol·licitar l\'accés." + "Sol·licita unir-t\'hi" + "Sí, activa el xifrat" + "Un cop activat, el xifrat d\'una sala no es pot desactivar. L\'històric de missatges només serà visible per als membres de la sala des que van ser convidats o des que s\'hi van unir. +Ningú a part dels membres de la sala podrà llegir els missatges. Això pot impedir que els bots i els ponts (\'bridges\') funcionin correctament. +No es recomana activar el xifrat a les sales que tothom pot trobar i unir-se." + "Vols activar el xifrat?" + "Un cop activat, el xifrat no es pot desactivar." + "Xifrat" + "Activa el xifrat d\'extrem a extrem" + "Tothom pot unir-s\'hi." + "Tothom" + "Només s\'hi poden unir les persones convidades." + "Només amb invitació" + "Accés" + "Actualment els espais no són compatibles" + "És necessària una adreça perquè sigui visible al directori públic." + "Adreça" + "Permet trobar aquesta sala cercant %1$s al directori públic de sales" + "Visible al directori públic" + "Tothom (historial públic)" + "Qui pot llegir l\'historial?" + "Membres, des de quan es van convidar" + "Membres (historial complet)" + "Les adreces de sala són maneres de trobar i accedir a les sales. Això també garanteix que puguis compartir fàcilment la teva sala amb altres persones. +Pots optar per publicar la teva sala al directori públic de sales del teu servidor local." + "Visibilitat" + "Seguretat i privadesa" + diff --git a/features/securityandprivacy/impl/src/main/res/values-da/translations.xml b/features/securityandprivacy/impl/src/main/res/values-da/translations.xml index 659799693b..566c26b4af 100644 --- a/features/securityandprivacy/impl/src/main/res/values-da/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-da/translations.xml @@ -29,7 +29,7 @@ Vi anbefaler ikke at aktivere kryptering for rum, som alle kan finde og deltage "Adgang" "Alle i autoriserede klynger kan deltage." "Alle i %1$s kan deltage." - "Medlemmer af rummet" + "Medlemmer af klyngen" "Klynger understøttes ikke i øjeblikket" "Du skal bruge en adresse for at gøre det synligt i det offentlige register." "Adresse" diff --git a/features/securityandprivacy/impl/src/main/res/values-pl/translations.xml b/features/securityandprivacy/impl/src/main/res/values-pl/translations.xml index c62c4f2af4..60415c2288 100644 --- a/features/securityandprivacy/impl/src/main/res/values-pl/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-pl/translations.xml @@ -1,10 +1,17 @@ - "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." - "Adres pokoju" - "Dodaj adres pokoju" - "Każdy może poprosić o dołączenie do pokoju, ale administrator lub moderator będzie musiał zatwierdzić żądanie." + "Aby pokój był widoczny w katalogu pokoi publicznych, potrzebny jest adres pokoju." + "Edytuj adres" + "Przestrzenie, których członkowie mogą dołączyć do pokoju bez zaproszenia." + "Zarządzaj przestrzeniami" + "(Nieznana przestrzeń)" + "Inne przestrzenie, których nie jesteś członkiem" + "Twoje przestrzenie" + "Dodaj adres" + "Każdy w autoryzowanych przestrzeniach może dołączyć, ale wszyscy inni muszą poprosić o dostęp." + "Każdy musi poprosić o dostęp." "Poproś o dołączenie" + "Każdy w %1$s może dołączyć, ale wszyscy pozostali muszą poprosić o dostęp." "Tak, włącz szyfrowanie" "Po włączeniu szyfrowanie pokoju nie może zostać wyłączone, a historia wiadomości będzie widoczna tylko dla członków od momentu, w którym dołączyli lub zostali zaproszeni. Nikt poza członkami pokoju nie będzie mógł czytać wiadomości. Może to wpłynąć na prawidłowe działanie botów lub mostków. @@ -13,23 +20,31 @@ Odradzamy włączanie szyfrowania dla pokoi, które każdy może znaleźć i do "Po włączeniu szyfrowania nie można wyłączyć." "Szyfrowanie" "Włącz szyfrowanie end-to-end" - "Każdy może znaleźć i dołączyć" + "Każdy może dołączyć." "Każdy" - "Tylko osoby z zaproszeniem mogą dołączyć" - "Tylko zaproszenie" - "Dostęp do pokoju" + "Wybierz, którzy członkowie przestrzeni mogą dołączyć do tego pokoju bez zaproszenia. %1$s" + "Zarządzaj przestrzeniami" + "Tylko zaproszone osoby mogą dołączyć" + "Tylko na zaproszenie" + "Dostęp" + "Każdy w autoryzowanych przestrzeniach może dołączyć." + "Każdy w %1$s może dołączyć." + "Członkowie przestrzeni" "Przestrzenie nie są obecnie wspierane" - "Aby pokój był widoczny w katalogu, potrzebny jest adres pokoju." - "Adres pokoju" + "Aby pokój był widoczny w katalogu pokoi publicznych, potrzebny jest adres pokoju." + "Adres" "Zezwól na znalezienie tego pokoju wyszukując %1$s w katalogu pokoi publicznych" + "Zezwól, by inni mogli Cię znaleźć, przeszukując katalog publiczny." "Widoczny w katalogu pokoi publicznych" - "Ktokolwiek" + "Każdy (historia jest publiczna)" + "Zmiany nie zmienią przeszłych wiadomości, tylko nowe. %1$s" "Kto może czytać historię" - "Od momentu kiedy członkowie zostali zaproszeni" - "Członkowie od momentu włączenia tej opcji" + "Członkowie od kiedy zostali zaproszeni" + "Członkowie (cała historia)" "Adresy pokoju umożliwiają łatwe znalezienie i dołączenie do pokojów. Również możesz się zdecydować na upublicznienie Twojego serwera w katalogu pokoi publicznych." "Publikowanie pokoju" - "Widoczność pokoju" + "Adresy pokoi pomagają w znalezieniu i dołączeniu do pokoi i przestrzeni. Umożliwiają również łatwe udostępnianie ich innym." + "Widoczność" "Bezpieczeństwo i prywatność" diff --git a/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml index 83292637ce..2f3eaab319 100644 --- a/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-ro/translations.xml @@ -10,6 +10,7 @@ "Adăugați o adresă" "Oricine se află în spațiile autorizate se poate alătura, dar toți ceilalți trebuie să solicite accesul." "Toată lumea trebuie să solicite acces." + "Solicitați să vă alăturați" "Oricine în %1$s se poate alătura, dar toți ceilalți trebuie să solicite acces." "Da, activați criptarea" "Odată activată, criptarea pentru o cameră nu poate fi dezactivată. Mesajele anterioare vor fi vizibile numai pentru membrii camerei de la momentul la care au fost invitați sau de la momentul la care s-au alăturat camerei. @@ -20,22 +21,26 @@ Nu recomandăm activarea criptării pentru camerele pe care oricine le poate gă "Criptare" "Activați criptarea end-to-end" "Oricine se poate alătura." - "Alegeți membrii căror spații se pot alătura acestei camere fără invitație. %1$s" + "Oricine" + "Alegeți membrii căror spații se pot alătura acestei cameră fără invitație. %1$s" "Gestionați spațiile" "Doar persoanele invitate se pot alătura." "Doar pe bază de invitație" "Acces" "Oricine se află într-un spațiu autorizat poate participa." "Oricine din %1$s se poate alătura." + "Membrii spațiului" "Spațiile nu sunt momentan suportate." "Veți avea nevoie de o adresă pentru a o face vizibilă în directorul public." "Adresă" "Permiteți găsirea acestei camere prin căutarea în directorul de camere publice al %1$s" "Permiteți găsirea prin căutarea în directorul public." "Vizibilă în directorul de camere publice" + "Oricine (istoricul este public)" + "Modificările nu vor afecta mesajele anterioare, ci doar pe cele noi. %1$s" "Cine poate citi mesajele anterioare" - "Doar pentru membri, de la momentul în care au fost invitați" - "Doar pentru membri, după selectarea acestei opțiuni" + "Membri de la momentul invitației" + "Membri (istoric complet)" "Adresele camerelor sunt modalități de a găsi și accesa camere. Acest lucru vă asigură, de asemenea, că puteți partaja cu ușurință camera dumneavoastră cu alte persoane. Puteți alege să publicați camera în directorul public al camerelor serverului dumneavoastră." "Publicare cameră" diff --git a/features/securityandprivacy/impl/src/main/res/values-ru/translations.xml b/features/securityandprivacy/impl/src/main/res/values-ru/translations.xml index 2cae719a03..bd6ad0081d 100644 --- a/features/securityandprivacy/impl/src/main/res/values-ru/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-ru/translations.xml @@ -3,7 +3,7 @@ "Необходимо задать адрес комнаты, чтобы опубликовать ее в каталоге комнат." "Редактировать адрес комнаты" "Пространства, где участники могут присоединиться к комнате без приглашения." - "Управление пространством" + "Управление пространствами" "(неизвестное пространство)" "Другие пространства, в которых вы не состоите" "Ваши пространства" @@ -23,7 +23,7 @@ "Любой желающий может присоединиться" "Кто угодно" "Выберите, участники каких пространств могут присоединиться к этой комнате без приглашения.%1$s" - "Управление пространством" + "Управление пространствами" "Присоединиться могут только приглашенные люди." "Только по приглашению" "Доступ" diff --git a/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml b/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml index f8b96ee6c5..0921887ccb 100644 --- a/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-uk/translations.xml @@ -8,8 +8,10 @@ "Інші простори, учасником яких ви не є" "Ваші простори" "Додати адресу" + "Будь-хто в авторизованих просторах може приєднатися, але всі інші повинні подати запит на доступ." "Усі повинні запитувати доступ." "Запит на приєднання" + "Будь-хто з %1$s може приєднатися, але всі інші повинні подати запит на доступ." "Так, увімкнути шифрування" "Після ввімкнення шифрування кімнати, його неможливо вимкнути, історію повідомлень бачитимуть лише учасники кімнати, яких було запрошено або які приєдналися до кімнати. Ніхто, крім учасників кімнати, не зможе прочитати повідомлення. Це може перешкоджати коректній роботі ботів і мостів. @@ -25,11 +27,14 @@ "Приєднатися можуть лише запрошені люди." "Лише запрошені" "Доступ" + "Долучитися може будь-хто, хто має доступ до авторизованих просторів." "Долучитися може будь-хто з %1$s." + "Учасники простору" "Простори наразі не підтримуються" "Вам знадобиться адреса кімнати, щоб зробити її видимою в каталозі." "Адреса" "Дозвольте, щоб цю кімнату можна було знайти за допомогою пошуку в каталозі загальнодоступних кімнат %1$s " + "Дозвольте знаходити вас за допомогою пошуку в публічному каталозі." "Видима в загальному каталозі" "Будь-хто (загальнодоступна історія)" "Зміни не вплинуть на попередні повідомлення, лише на нові. %1$s" @@ -39,6 +44,7 @@ "Адреси кімнат — це спосіб знайти кімнату та отримати до неї доступ. Це також гарантує, що ви можете легко поділитися своєю кімнатою з іншими. Ви можете опублікувати свою кімнату в каталозі загальнодоступних кімнат вашого домашнього сервера." "Публікація в кімнаті" + "Адреси — це спосіб знаходити кімнати та простори та отримувати до них доступ. Це також гарантує, що ви зможете легко ділитися ними з іншими." "Видимість" "Безпека й приватність" diff --git a/features/securityandprivacy/impl/src/main/res/values-zh/translations.xml b/features/securityandprivacy/impl/src/main/res/values-zh/translations.xml index dda5834ba7..09a22f06b3 100644 --- a/features/securityandprivacy/impl/src/main/res/values-zh/translations.xml +++ b/features/securityandprivacy/impl/src/main/res/values-zh/translations.xml @@ -24,7 +24,7 @@ "任何人" "选择哪些无需邀请即可加入此房间的空间成员。%1$s" "管理空间" - "仅限受邀者加入。" + "仅限受邀人员加入。" "仅限受邀者" "访问权限" "任何位于已授权空间的成员均可加入。" diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt index a6f21c0162..5578d6b01c 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/SecurityAndPrivacyFlowNodeTest.kt @@ -8,7 +8,6 @@ package io.element.android.features.securityandprivacy.impl -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumble.appyx.core.modality.AncestryInfo import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.navmodel.backstack.activeElement @@ -20,12 +19,11 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule 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 +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class SecurityAndPrivacyFlowNodeTest { +class SecurityAndPrivacyFlowNodeTest : RobolectricTest() { @Test fun `initial backstack contains SecurityAndPrivacy`() = runTest { val flowNode = createFlowNode() diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt index 17d6f3a88d..1d0886f52a 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securityandprivacy.impl.editroomaddress import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity import io.element.android.libraries.testtags.TestTags @@ -23,86 +25,81 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class EditRoomAddressViewTest { - @get:Rule val rule = createAndroidComposeRule() +class EditRoomAddressViewTest : RobolectricTest() { @Test - fun `click on back invokes expected callback`() { + fun `click on back invokes expected callback`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setEditRoomAddressView(onBackClick = callback) - rule.pressBack() + setEditRoomAddressView(onBackClick = callback) + pressBack() } } @Test - fun `click on disabled save doesn't emit event`() { + fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = anEditRoomAddressState(eventSink = recorder) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_save) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_save) recorder.assertEmpty() } @Test - fun `click on enabled save emits the expected event`() { + fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "room", roomAddressValidity = RoomAddressValidity.Valid, eventSink = recorder ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_save) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_save) recorder.assertSingle(EditRoomAddressEvents.Save) } @Test - fun `text changes on text field emits the expected event`() { + fun `text changes on text field emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", eventSink = recorder ) - rule.setEditRoomAddressView(state) + setEditRoomAddressView(state) - rule.onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") + onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) } @Test - fun `click on dismiss error emits the expected event`() { + fun `click on dismiss error emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), eventSink = recorder ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_cancel) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_cancel) recorder.assertSingle(EditRoomAddressEvents.DismissError) } @Test - fun `click on retry error emits the expected event`() { + fun `click on retry error emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), eventSink = recorder ) - rule.setEditRoomAddressView(state) - rule.clickOn(CommonStrings.action_retry) + setEditRoomAddressView(state) + clickOn(CommonStrings.action_retry) recorder.assertSingle(EditRoomAddressEvents.Save) } } -private fun AndroidComposeTestRule.setEditRoomAddressView( +private fun AndroidComposeUiTest.setEditRoomAddressView( state: EditRoomAddressState = anEditRoomAddressState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt index c732df6df0..7d0a1aea16 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securityandprivacy.impl.manageauthorizedspaces import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -22,28 +24,23 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class ManageAuthorizedSpacesViewTest { - @get:Rule val rule = createAndroidComposeRule() +class ManageAuthorizedSpacesViewTest : RobolectricTest() { @Test - fun `clicking back emits Cancel event`() { + fun `clicking back emits Cancel event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState(eventSink = recorder) - rule.setManageAuthorizedSpacesView(state) - rule.pressBack() + setManageAuthorizedSpacesView(state) + pressBack() recorder.assertSingle(ManageAuthorizedSpacesEvent.Cancel) } @Test - fun `clicking space checkbox emits ToggleSpace event`() { + fun `clicking space checkbox emits ToggleSpace event`() = runAndroidComposeUiTest { val roomId = A_ROOM_ID val space = aSpaceRoom(roomId = roomId, displayName = "Test Space") val recorder = EventsRecorder() @@ -51,37 +48,37 @@ class ManageAuthorizedSpacesViewTest { selectableSpaces = listOf(space), eventSink = recorder ) - rule.setManageAuthorizedSpacesView(state) - rule.onNodeWithText("Test Space").performClick() + setManageAuthorizedSpacesView(state) + onNodeWithText("Test Space").performClick() recorder.assertSingle(ManageAuthorizedSpacesEvent.ToggleSpace(roomId)) } @Test - fun `clicking done button emits Done event`() { + fun `clicking done button emits Done event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aManageAuthorizedSpacesState( selectedIds = listOf(A_ROOM_ID), eventSink = recorder ) - rule.setManageAuthorizedSpacesView(state) - rule.clickOn(CommonStrings.action_done) + setManageAuthorizedSpacesView(state) + clickOn(CommonStrings.action_done) recorder.assertSingle(ManageAuthorizedSpacesEvent.Done) } @Test - fun `done button is disabled when no spaces selected`() { + fun `done button is disabled when no spaces selected`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = aManageAuthorizedSpacesState( selectedIds = emptyList(), eventSink = recorder ) - rule.setManageAuthorizedSpacesView(state) - rule.clickOn(CommonStrings.action_done) + setManageAuthorizedSpacesView(state) + clickOn(CommonStrings.action_done) recorder.assertEmpty() } } -private fun AndroidComposeTestRule.setManageAuthorizedSpacesView( +private fun AndroidComposeUiTest.setManageAuthorizedSpacesView( state: ManageAuthorizedSpacesState = aManageAuthorizedSpacesState( eventSink = EventsRecorder(expectEvents = false) ), diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt index a1f46b2938..220a8d3d6d 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyViewTest.kt @@ -5,14 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.securityandprivacy.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.securityandprivacy.impl.R import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -22,74 +24,69 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.persistentListOf -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 SecurityAndPrivacyViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class SecurityAndPrivacyViewTest : RobolectricTest() { @Test - fun `click on back invokes emits the expected event`() { + fun `click on back invokes emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.pressBack() + setSecurityAndPrivacyView(state) + pressBack() recorder.assertSingle(SecurityAndPrivacyEvent.Exit) } @Test - fun `discard cancellation emits the expected event`() { + fun `discard cancellation emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_discard) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_discard) recorder.assertSingle(SecurityAndPrivacyEvent.Exit) } @Test - fun `save cancellation confirmation emits the expected event`() { + fun `save cancellation confirmation emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( saveAction = AsyncAction.ConfirmingCancellation, eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_save, inDialog = true) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_save, inDialog = true) recorder.assertSingle(SecurityAndPrivacyEvent.Save) } @Test - fun `click on room access item emits the expected event`() { + fun `click on room access item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) } @Test - fun `click on disabled save doesn't emit event`() { + fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = aSecurityAndPrivacyState(eventSink = recorder) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_save) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_save) recorder.assertEmpty() } @Test - fun `click on enabled save emits the expected event`() { + fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -97,14 +94,14 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.Anyone, ) ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(CommonStrings.action_save) + setSecurityAndPrivacyView(state) + clickOn(CommonStrings.action_save) recorder.assertSingle(SecurityAndPrivacyEvent.Save) } @Test @Config(qualifiers = "h640dp") - fun `click on room address item emits the expected event`() { + fun `click on room address item emits the expected event`() = runAndroidComposeUiTest { val address = "@alias:matrix.org" val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( @@ -114,14 +111,14 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.Anyone, ), ) - rule.setSecurityAndPrivacyView(state) - rule.onNodeWithText(address).performClick() + setSecurityAndPrivacyView(state) + onNodeWithText(address).performClick() recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress) } @Test @Config(qualifiers = "h1024dp") - fun `click on room visibility item emits the expected event`() { + fun `click on room visibility item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -130,14 +127,14 @@ class SecurityAndPrivacyViewTest { isVisibleInRoomDirectory = AsyncData.Success(false), ), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleRoomVisibility) } @Test @Config(qualifiers = "h1024dp") - fun `click on history visibility item emits the expected event`() { + fun `click on history visibility item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -145,65 +142,65 @@ class SecurityAndPrivacyViewTest { historyVisibility = SecurityAndPrivacyHistoryVisibility.Invited, ), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_history_since_invite_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Invited)) } @Test @Config(qualifiers = "h1024dp") - fun `click on encryption item emits the expected event`() { + fun `click on encryption item emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, savedSettings = aSecurityAndPrivacySettings(isEncrypted = false), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState) } @Test - fun `click on encryption confirm emits the expected event`() { + fun `click on encryption confirm emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, showEncryptionConfirmation = true, ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption) } @Test @Config(qualifiers = "h1024dp") - fun `click on space member access emits the expected event`() { + fun `click on space member access emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_room_access_space_members_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.SelectSpaceMemberAccess) } @Test @Config(qualifiers = "h1024dp") - fun `click on ask to join with space members emits the expected event`() { + fun `click on ask to join with space members emits the expected event`() = runAndroidComposeUiTest { val recorder = EventsRecorder() val state = aSecurityAndPrivacyState( eventSink = recorder, spaceSelectionMode = SpaceSelectionMode.Single(A_ROOM_ID, null), ) - rule.setSecurityAndPrivacyView(state) - rule.clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) + setSecurityAndPrivacyView(state) + clickOn(R.string.screen_security_and_privacy_ask_to_join_option_title) recorder.assertSingle(SecurityAndPrivacyEvent.SelectAskToJoinWithSpaceMembersAccess) } @Test @Config(qualifiers = "h1024dp") - fun `manage spaces footer is shown when space member access is selected`() { + fun `manage spaces footer is shown when space member access is selected`() = runAndroidComposeUiTest { val recorder = EventsRecorder(expectEvents = false) val state = aSecurityAndPrivacyState( eventSink = recorder, @@ -212,15 +209,16 @@ class SecurityAndPrivacyViewTest { roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember(persistentListOf(A_ROOM_ID)), ), ) - rule.setSecurityAndPrivacyView(state) + setSecurityAndPrivacyView(state) // The footer text uses AnnotatedString with a link. Verify the footer text is displayed. - val actionFooterText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action) - val footerText = rule.activity.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText) - rule.onNodeWithText(footerText).assertExists() + val resources = activity!!.resources + val actionFooterText = resources.getString(R.string.screen_security_and_privacy_room_access_footer_manage_spaces_action) + val footerText = resources.getString(R.string.screen_security_and_privacy_room_access_footer, actionFooterText) + onNodeWithText(footerText).assertExists() } } -private fun AndroidComposeTestRule.setSecurityAndPrivacyView( +private fun AndroidComposeUiTest.setSecurityAndPrivacyView( state: SecurityAndPrivacyState = aSecurityAndPrivacyState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultOnSharedData.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultOnSharedData.kt index 3a71f02dc3..daab3787b3 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultOnSharedData.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultOnSharedData.kt @@ -7,6 +7,7 @@ package io.element.android.features.share.impl +import android.content.ContentResolver import android.content.Context import android.content.Intent import android.net.Uri @@ -15,13 +16,14 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.features.share.api.OnSharedData import io.element.android.features.share.api.ShareIntentData +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber -import kotlin.collections.forEach @ContributesBinding(AppScope::class) class DefaultOnSharedData( @ApplicationContext private val context: Context, + private val buildMeta: BuildMeta, ) : OnSharedData { override fun invoke(data: ShareIntentData) { when (data) { @@ -29,6 +31,13 @@ class DefaultOnSharedData( // No-op, there is nothing to do for plain text intents. } is ShareIntentData.Uris -> { + val fileProvider = "${buildMeta.applicationId}.fileprovider" + for (sharedUri in data.uris) { + // Remove the local copy of the shared file, as it is not needed anymore + if (sharedUri.uri.scheme == ContentResolver.SCHEME_CONTENT && sharedUri.uri.host == fileProvider) { + context.contentResolver.delete(sharedUri.uri, null, emptyArray()) + } + } revokeUriPermissions(data.uris.map { it.uri }) } } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt index 0597b3b678..1abac1afa6 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareNode.kt @@ -70,7 +70,10 @@ class ShareNode( return roomSelectEntryPoint.createNode( parentNode = this, buildContext = buildContext, - params = RoomSelectEntryPoint.Params(mode = RoomSelectMode.Share), + params = RoomSelectEntryPoint.Params( + mode = RoomSelectMode.Share, + maxNumberOfRooms = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS, + ), callback = callback, ) } 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 fd2c8ff194..ab3a260c01 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 @@ -32,15 +32,13 @@ import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class SharePresenterTest { +class SharePresenterTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() diff --git a/features/signedout/impl/build.gradle.kts b/features/signedout/impl/build.gradle.kts index 3c8aac5e25..b3801288be 100644 --- a/features/signedout/impl/build.gradle.kts +++ b/features/signedout/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) testCommonDependencies(libs) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index 396339adbb..95a57db93e 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -36,7 +36,7 @@ private fun aSessionData( accessToken = "anAccessToken", refreshToken = "aRefreshToken", homeserverUrl = "aHomeserverUrl", - oidcData = null, + oAuthData = null, loginTimestamp = null, isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, diff --git a/features/signedout/impl/src/main/res/values-ca/translations.xml b/features/signedout/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..6cba810df9 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,8 @@ + + + "Has canviat la contrasenya en una altra sessió" + "Has eliminat la sessió des d\'una altra sessió" + "L\'administrador del teu servidor t\'ha invalidat l\'accés" + "S\'ha tancat la sessió per algun dels motius enumerats a continuació. Torna a iniciar sessió per continuar utilitzant %s." + "Has tancat sessió" + diff --git a/features/signedout/impl/src/main/res/values-ru/translations.xml b/features/signedout/impl/src/main/res/values-ru/translations.xml index d3fd510afd..042cd0ca08 100644 --- a/features/signedout/impl/src/main/res/values-ru/translations.xml +++ b/features/signedout/impl/src/main/res/values-ru/translations.xml @@ -3,6 +3,6 @@ "Вы изменили свой пароль в другой сессии" "Вы удалили сессию из другой сессии" "Администратор вашего сервера аннулировал ваш доступ" - "Возможно, вы вышли из системы по одной из причин, перечисленных ниже. Пожалуйста, войдите в систему еще раз, чтобы продолжить использование %s." + "Возможно, Вы вышли из системы по одной из причин, перечисленных ниже. Пожалуйста, войдите в систему ещё раз, чтобы продолжить использовать %s." "Вы вышли из системы" diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt index dadd54cf6f..c7aea55488 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceNode.kt @@ -41,7 +41,7 @@ class AddRoomToSpaceNode( AddRoomToSpaceView( state = state, onBackClick = callback::onFinish, - onRoomsAdded = callback::onFinish, + onAddRoom = callback::onFinish, modifier = modifier ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt index 10c70d6f7d..882db26f00 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceSearchDataSource.kt @@ -15,7 +15,6 @@ 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.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomInfo -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.recent.getRecentlyVisitedRoomInfoFlow import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListFilter diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index c64efa8074..47c78e6fb8 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -23,6 +23,8 @@ 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.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter @@ -57,9 +59,10 @@ import kotlinx.collections.immutable.toImmutableList fun AddRoomToSpaceView( state: AddRoomToSpaceState, onBackClick: () -> Unit, - onRoomsAdded: () -> Unit, + onAddRoom: () -> Unit, modifier: Modifier = Modifier, ) { + val updatedOnAddRoom by rememberUpdatedState(onAddRoom) fun onRoomToggled(room: SelectRoomInfo) { state.eventSink(AddRoomToSpaceEvent.ToggleRoom(room)) } @@ -78,7 +81,7 @@ fun AddRoomToSpaceView( // Navigate back on success LaunchedEffect(state.saveAction) { if (state.saveAction is AsyncAction.Success) { - onRoomsAdded() + updatedOnAddRoom() } } @@ -247,6 +250,6 @@ internal fun AddRoomToSpaceViewPreview( AddRoomToSpaceView( state = state, onBackClick = {}, - onRoomsAdded = {}, + onAddRoom = {}, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt index 4e1fd34673..e0876c255c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt @@ -67,7 +67,7 @@ class LeaveSpacePresenter( .orEmpty() .partition { it.spaceRoom.roomId == leaveSpaceHandle.id } // By default select all rooms that can be left - val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDirect != true } + val otherRoomsExcludingDm = otherRooms.filter { it.spaceRoom.isDm != true } selectedRoomIds = otherRoomsExcludingDm .filter { it.isLastOwner.not() } .map { it.spaceRoom.roomId } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt index 3d8df360a7..ddf34cb076 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt @@ -11,6 +11,7 @@ package io.element.android.features.space.impl.leave import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.preview.SPACE_NAME import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.previewutils.room.aSpaceRoom @@ -117,7 +118,7 @@ class LeaveSpaceStateProvider : PreviewParameterProvider { } fun aLeaveSpaceState( - spaceName: String? = "Space name", + spaceName: String? = SPACE_NAME, isLastOwner: Boolean = false, areCreatorsPrivileged: Boolean = false, selectableSpaceRooms: AsyncData> = AsyncData.Uninitialized, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index c43257b383..6824cc6c10 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -109,6 +109,7 @@ private fun aSpaceInfo( avatarUrl = null, isPublic = true, isDirect = false, + isDm = false, isEncrypted = false, joinRule = joinRule, isSpace = true, @@ -139,6 +140,7 @@ private fun aSpaceInfo( privilegedCreatorRole = false, isLowPriority = false, activeCallIntentConsensus = CallIntentConsensus.None, + fullyReadEventId = null, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index 05fb75ee5e..29898a1f63 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -354,7 +354,8 @@ private fun EmptySpaceView( title = stringResource(R.string.screen_space_empty_state_title), subTitle = null, iconStyle = BigIcon.Style.Default(vectorIcon = CompoundIcons.Room(), usePrimaryTint = true), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 24.dp), ) ButtonColumnMolecule( @@ -425,6 +426,7 @@ private fun SpaceViewTopBar( modifier = Modifier .clip(roundedCornerShape) .clickable(enabled = canAccessSpaceSettings, onClick = onSettingsClick) + .semantics { heading() } ) }, actions = { @@ -532,6 +534,7 @@ private fun ManageModeTopBar( Text( text = pluralStringResource(CommonPlurals.common_selected_count, selectedCount, selectedCount), style = ElementTheme.typography.fontBodyLgMedium, + modifier = Modifier.semantics { heading() }, ) }, actions = { @@ -585,10 +588,7 @@ private fun SpaceAvatarAndNameRow( ) Text( modifier = Modifier - .padding(horizontal = 8.dp) - .semantics { - heading() - }, + .padding(horizontal = 8.dp), text = name ?: stringResource(CommonStrings.common_no_space_name), style = ElementTheme.typography.fontBodyLgMedium, fontStyle = FontStyle.Italic.takeIf { name == null }, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt index 2030b6885a..36f6a2a1d0 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt @@ -9,6 +9,7 @@ package io.element.android.features.space.impl.settings import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.SPACE_NAME import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId @@ -24,7 +25,7 @@ open class SpaceSettingsStateProvider : PreviewParameterProvider + + "Rols i permisos" + "Seguretat i privadesa" + diff --git a/features/space/impl/src/main/res/values-fa/translations.xml b/features/space/impl/src/main/res/values-fa/translations.xml index dcab5c912a..d8f377c0b2 100644 --- a/features/space/impl/src/main/res/values-fa/translations.xml +++ b/features/space/impl/src/main/res/values-fa/translations.xml @@ -8,6 +8,6 @@ "تنها مدیر %1$s هستید" "دیدن اعضا" "ترک فضا" - "نقش‌ها و اجازه‌ها" + "نقش‌ها و مجوزها" "امنیت و محرمانگی" diff --git a/features/space/impl/src/main/res/values-pl/translations.xml b/features/space/impl/src/main/res/values-pl/translations.xml index f1a964c6e3..9c3097b75f 100644 --- a/features/space/impl/src/main/res/values-pl/translations.xml +++ b/features/space/impl/src/main/res/values-pl/translations.xml @@ -9,9 +9,21 @@ "Wybierz pokoje, które chcesz opuścić, a których nie jesteś jedynym administratorem:" "Aby opuścić tę przestrzeń, musisz przypisać do niej innego administratora." + "Jesteś jedynym właścicielem %1$s. Musisz przenieść własność zanim odejdziesz." "Nie zostaniesz usunięty z następujących pokoi, ponieważ jesteś ich jedynym administratorem:" "Opuścić %1$s?" "Jesteś jedynym administratorem %1$s" + "Przenieś własność" + "Pokój" + "Dodanie pokoju nie wpłynie na dostęp do niego. Aby zmienić ustawienia dostępu, przejdź do Ustawienia pokoju > Bezpieczeństwo i prywatność." + "Dodaj swój pierwszy pokój" + "Zobacz członków" + "Usunięcie pokoju nie wpłynie na dostęp do niego. Aby zmienić ustawienia dostępu, przejdź do Ustawienia pokoju > Bezpieczeństwo i prywatność." + + "Usuń %1$d pokój z %2$s" + "Usuń %1$d pokoje z %2$s" + "Usuń %1$d pokoi z %2$s" + "Opuść przestrzeń" "Role i uprawnienia" "Bezpieczeństwo i prywatność" diff --git a/features/space/impl/src/main/res/values-ro/translations.xml b/features/space/impl/src/main/res/values-ro/translations.xml index 2bf5d0e251..7748f44ee2 100644 --- a/features/space/impl/src/main/res/values-ro/translations.xml +++ b/features/space/impl/src/main/res/values-ro/translations.xml @@ -9,10 +9,21 @@ "Selectați camerele pe care doriți să le părăsiți și în care nu sunteți singurul administrator:" "Trebuie să desemnați un alt administrator pentru acest spațiu înainte de a-l părăsi." + "Sunteți singurul proprietar al %1$s. Trebuie să transferați dreptul de proprietate către altcineva înainte de a parăsi camera." "Nu veți părăsi următoarele camere deoarece sunteți singurul administrator:" "Părăsiți %1$s?" "Sunteți singurul administrator pentru %1$s" + "Transferați proprietatea" + "Cameră" + "Adăugarea unei camere nu va afecta accesul la cameră. Pentru a modifica accesul, accesați Setări cameră > Securitate și confidențialitate." + "Adăugați prima dumneavoastră cameră" "Vizualizați membrii" + "Eliminarea unei camere nu va afecta accesul la aceasta. Pentru a modifica accesul, accesați Informații despre cameră > Confidențialitate și securitate." + + "Eliminați camera %1$d din %2$s" + "Eliminați camerele %1$d din %2$s" + "Eliminați camerele %1$d din %2$s" + "Părăsiți spațiul" "Roluri și permisiuni" "Securitate & confidențialitate" diff --git a/features/space/impl/src/main/res/values-uk/translations.xml b/features/space/impl/src/main/res/values-uk/translations.xml index e124f72826..df4b4fb922 100644 --- a/features/space/impl/src/main/res/values-uk/translations.xml +++ b/features/space/impl/src/main/res/values-uk/translations.xml @@ -2,14 +2,28 @@ "Оберіть власників" "%1$s (Адміністратор)" + + "Залишити %1$d кімнату та простір" + "Залишити %1$d кімнату та простори" + "Залишити %1$d кімнат та просторів" + "Виберіть кімнати, з яких ви хочете вийти, і в них ви не єдиний адміністратор:" "Перш ніж ви зможете вийти, вам потрібно призначити іншого адміністратора для цього простору." + "Ви єдиний власник %1$s. Перш ніж піти, вам потрібно передати право володіння комусь іншому." "Вас не буде видалено з цих кімнат, оскільки ви єдиний адміністратор:" "Вийти з %1$s?" "Ви єдиний адміністратор у %1$s" + "Передача права володіння" "Кімната" + "Додавання кімнати не вплине на доступ до неї. Щоб змінити доступ, перейдіть до «Налаштування кімнати» > «Безпека та конфіденційність»." "Додайте свою першу кімнату" "Переглянути учасників" + "Видалення кімнати не вплине на доступ до неї. Щоб змінити доступ, перейдіть до розділу «Налаштування кімнати» > «Безпека та конфіденційність»." + + "Видалити %1$d кімнату з %2$s" + "Видалити %1$d кімнати з %2$s" + "Видалити %1$d кімнат з %2$s" + "Вийти з простору" "Ролі та дозволи" "Безпека й приватність" diff --git a/features/space/impl/src/main/res/values-zh/translations.xml b/features/space/impl/src/main/res/values-zh/translations.xml index 784ab4bff4..bf5c298590 100644 --- a/features/space/impl/src/main/res/values-zh/translations.xml +++ b/features/space/impl/src/main/res/values-zh/translations.xml @@ -1,7 +1,7 @@ "选择所有者" - "%1$s (管理员)" + "%1$s(管理员)" "离开 %1$d 个房间和空间" @@ -16,7 +16,7 @@ "添加房间不会影响其访问权限。如需更改访问权限,请前往“房间设置” > “安全与隐私”。" "添加第一个房间" "查看成员" - "移除房间不会影响其访问权限。要更改访问权限,请转到“房间信息” > “隐私和安全”。" + "移除房间不会影响其访问权限。要更改访问权限,请转到“房间信息”→“隐私与安全”。" "移除 %2$s 个房间,共 %1$d 个" diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt index d75fecd05a..c49144f1c9 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceViewTest.kt @@ -5,14 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.space.impl.addroom import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.ui.strings.CommonStrings @@ -21,78 +23,73 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.toImmutableList -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 AddRoomToSpaceViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class AddRoomToSpaceViewTest : RobolectricTest() { @Test - fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() { + fun `clicking back when search inactive emits Dismiss and invokes onBackClick`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() ensureCalledOnce { - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = false, eventSink = eventsRecorder, ), onBackClick = it, ) - rule.pressBack() + pressBack() } eventsRecorder.assertSingle(AddRoomToSpaceEvent.Dismiss) } @Test - fun `clicking back when search active emits CloseSearch event`() { + fun `clicking back when search active emits CloseSearch event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = true, eventSink = eventsRecorder, ), ) - rule.pressBack() + pressBack() eventsRecorder.assertSingle(AddRoomToSpaceEvent.OnSearchActiveChanged(false)) } @Test - fun `clicking save emits Save event`() { + fun `clicking save emits Save event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( selectedRooms = aSelectRoomInfoList().take(1).toImmutableList(), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_save) + clickOn(CommonStrings.action_save) eventsRecorder.assertSingle(AddRoomToSpaceEvent.Save) } @Config(qualifiers = "h1024dp") @Test - fun `clicking room in suggestions emits ToggleRoom event`() { + fun `clicking room in suggestions emits ToggleRoom event`() = runAndroidComposeUiTest { val suggestions = aSelectRoomInfoList() val eventsRecorder = EventsRecorder() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( suggestions = suggestions, eventSink = eventsRecorder, ), ) - rule.onNodeWithText(suggestions.first().name!!).performClick() + onNodeWithText(suggestions.first().name!!).performClick() eventsRecorder.assertSingle(AddRoomToSpaceEvent.ToggleRoom(suggestions.first())) } @Test - fun `onRoomsAdded called when saveAction is Success`() { + fun `onRoomsAdded called when saveAction is Success`() = runAndroidComposeUiTest { ensureCalledOnce { - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( saveAction = AsyncAction.Success(Unit), ), @@ -103,10 +100,10 @@ class AddRoomToSpaceViewTest { @Config(qualifiers = "h1024dp") @Test - fun `displaying search results sends UpdateSearchVisibleRange event`() { + fun `displaying search results sends UpdateSearchVisibleRange event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val rooms = aSelectRoomInfoList() - rule.setAddRoomToSpaceView( + setAddRoomToSpaceView( anAddRoomToSpaceState( isSearchActive = true, searchResults = SearchBarResultState.Results(rooms), @@ -117,7 +114,7 @@ class AddRoomToSpaceViewTest { } } -private fun AndroidComposeTestRule.setAddRoomToSpaceView( +private fun AndroidComposeUiTest.setAddRoomToSpaceView( state: AddRoomToSpaceState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomsAdded: () -> Unit = EnsureNeverCalled(), @@ -126,7 +123,7 @@ private fun AndroidComposeTestRule.setAddRo AddRoomToSpaceView( state = state, onBackClick = onBackClick, - onRoomsAdded = onRoomsAdded, + onAddRoom = onRoomsAdded, ) } } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt index b3b6fc7976..021f52defd 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt @@ -98,13 +98,13 @@ class LeaveSpacePresenterTest { listOf( aLeaveSpaceRoom(spaceRoom = aSpace), aLeaveSpaceRoom( - spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDirect = false) + spaceRoom = aSpaceRoom(roomId = A_ROOM_ID, isDm = false) ), aLeaveSpaceRoom( - spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDirect = true) + spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_2, isDm = true) ), aLeaveSpaceRoom( - spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDirect = null) + spaceRoom = aSpaceRoom(roomId = A_ROOM_ID_3, isDm = null) ), ) ) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 87343b6e34..042d6a00ff 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -6,15 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.space.impl.root import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.spaces.SpaceRoom @@ -33,37 +35,32 @@ 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.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class SpaceViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class SpaceViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, eventSink = eventsRecorder, ), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on a room name invokes the expected callback`() { + fun `clicking on a room name invokes the expected callback`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(aSpaceRoom) { - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, @@ -71,91 +68,91 @@ class SpaceViewTest { ), onRoomClick = it, ) - rule.onNodeWithText(A_ROOM_NAME).performClick() + onNodeWithText(A_ROOM_NAME).performClick() } } @Test - fun `clicking on Join room emits the expected Event`() { + fun `clicking on Join room emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_join) + clickOn(CommonStrings.action_join) eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on accept invite emits the expected Event`() { + fun `clicking on accept invite emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, children = listOf(aSpaceRoom), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_accept) + clickOn(CommonStrings.action_accept) eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on decline invite emits the expected Event`() { + fun `clicking on decline invite emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, children = listOf(aSpaceRoom), eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_decline) + clickOn(CommonStrings.action_decline) eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on topic emits the expected Event`() { + fun `clicking on topic emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC), hasMoreToLoad = false, eventSink = eventsRecorder, ) ) - rule.onNodeWithText(A_ROOM_TOPIC).performClick() + onNodeWithText(A_ROOM_TOPIC).performClick() eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) } @Test - fun `clicking back in manage mode emits ExitManageMode event`() { + fun `clicking back in manage mode emits ExitManageMode event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( hasMoreToLoad = false, isManageMode = true, eventSink = eventsRecorder, ) ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) } @Test - fun `clicking on room in manage mode emits ToggleRoomSelection event`() { + fun `clicking on room in manage mode emits ToggleRoomSelection event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom), hasMoreToLoad = false, @@ -163,14 +160,14 @@ class SpaceViewTest { eventSink = eventsRecorder, ) ) - rule.onNodeWithText(A_ROOM_NAME).performClick() + onNodeWithText(A_ROOM_NAME).performClick() eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) } @Test - fun `clicking remove button emits RemoveSelectedRooms event`() { + fun `clicking remove button emits RemoveSelectedRooms event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), hasMoreToLoad = false, @@ -179,15 +176,15 @@ class SpaceViewTest { eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_remove) + clickOn(CommonStrings.action_remove) eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) } @Config(qualifiers = "h1024dp") @Test - fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() { + fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setSpaceView( + setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), hasMoreToLoad = false, @@ -198,14 +195,14 @@ class SpaceViewTest { ) ) // Click on the Remove button in the confirmation dialog - rule.clickOn(CommonStrings.action_remove, inDialog = true) + clickOn(CommonStrings.action_remove, inDialog = true) eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) } @Test - fun `clicking create room button calls the expected callback`() { + fun `clicking create room button calls the expected callback`() = runAndroidComposeUiTest { val onCreateRoomClick = lambdaRecorder { } - rule.setSpaceView( + setSpaceView( aSpaceState( children = emptyList(), hasMoreToLoad = false, @@ -214,14 +211,14 @@ class SpaceViewTest { ), onCreateRoomClick = onCreateRoomClick, ) - rule.clickOn(CommonStrings.action_create_room) + clickOn(CommonStrings.action_create_room) onCreateRoomClick.assertions().isCalledOnce() } @Test - fun `clicking add existing room button calls the expected callback`() { + fun `clicking add existing room button calls the expected callback`() = runAndroidComposeUiTest { val onAddRoomClick = lambdaRecorder { } - rule.setSpaceView( + setSpaceView( aSpaceState( children = emptyList(), hasMoreToLoad = false, @@ -230,12 +227,12 @@ class SpaceViewTest { ), onAddRoomClick = onAddRoomClick, ) - rule.clickOn(CommonStrings.action_add_existing_rooms) + clickOn(CommonStrings.action_add_existing_rooms) onAddRoomClick.assertions().isCalledOnce() } } -private fun AndroidComposeTestRule.setSpaceView( +private fun AndroidComposeUiTest.setSpaceView( state: SpaceState, onBackClick: () -> Unit = EnsureNeverCalled(), onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(), 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..079ece4180 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 @@ -10,8 +10,6 @@ package io.element.android.features.startchat.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -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 @@ -24,8 +22,6 @@ import io.element.android.features.startchat.impl.userlist.UserListPresenterArgs 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.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.usersearch.api.UserRepository import kotlinx.coroutines.launch @@ -37,7 +33,6 @@ class StartChatPresenter( userListDataStore: UserListDataStore, private val startDMAction: StartDMAction, private val buildMeta: BuildMeta, - private val featureFlagService: FeatureFlagService, ) : Presenter { private val presenter = presenterFactory.create( UserListPresenterArgs( @@ -54,10 +49,6 @@ class StartChatPresenter( val localCoroutineScope = rememberCoroutineScope() val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - val isRoomDirectorySearchEnabled by remember { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch) - }.collectAsState(initial = false) - fun handleEvent(event: StartChatEvents) { when (event) { is StartChatEvents.StartDM -> localCoroutineScope.launch { @@ -75,7 +66,6 @@ class StartChatPresenter( applicationName = buildMeta.applicationName, userListState = userListState, startDmAction = startDmActionState.value, - isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, 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..e6746e1302 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 @@ -16,6 +16,5 @@ data class StartChatState( val applicationName: String, val userListState: UserListState, val startDmAction: AsyncAction, - val isRoomDirectorySearchEnabled: 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 a1e8f9d4f0..41a3551d1d 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 @@ -55,9 +55,6 @@ open class StartChatStateProvider : PreviewParameterProvider { aCreateRoomRootState( startDmAction = aConfirmingStartDmWithMatrixUser() ), - aCreateRoomRootState( - isRoomDirectorySearchEnabled = true, - ), ) } @@ -75,12 +72,10 @@ fun aCreateRoomRootState( applicationName: String = "Element X Preview", userListState: UserListState = aUserListState(), startDmAction: AsyncAction = AsyncAction.Uninitialized, - isRoomDirectorySearchEnabled: Boolean = false, eventSink: (StartChatEvents) -> Unit = {}, ) = StartChatState( applicationName = applicationName, userListState = userListState, startDmAction = startDmAction, - isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, 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 e077cbe82e..7537fee524 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 @@ -176,14 +176,12 @@ private fun CreateRoomActionButtonsList( onClick = onNewRoomClick, ) } - if (state.isRoomDirectorySearchEnabled) { - item { - CreateRoomActionButton( - iconRes = CompoundDrawables.ic_compound_list_bulleted, - text = stringResource(id = R.string.screen_room_directory_search_title), - onClick = onRoomDirectorySearchClick, - ) - } + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_list_bulleted, + text = stringResource(id = R.string.screen_room_directory_search_title), + onClick = onRoomDirectorySearchClick, + ) } item { CreateRoomActionButton( diff --git a/features/startchat/impl/src/main/res/values-ca/translations.xml b/features/startchat/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..e249d61582 --- /dev/null +++ b/features/startchat/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,12 @@ + + + "Sala nova" + "Directori de sales" + "S\'ha produït un error en intentar iniciar un xat" + "Uneix-te a una sala mitjançant l\'adreça" + "Adreça invàlida" + "Introdueix…" + "S\'ha trobat una sala coincident" + "Sala no trobada" + "p. ex. #nom-sala:matrix.org" + diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt index 92162ca82c..421f8c7089 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinBaseRoomByAddressViewTest.kt @@ -6,56 +6,52 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.startchat.impl.joinbyaddress import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performTextInput -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.startchat.impl.R import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class JoinBaseRoomByAddressViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class JoinBaseRoomByAddressViewTest : RobolectricTest() { @Test - fun `entering text emits the expected event`() { + fun `entering text emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomByAddressView( + setJoinRoomByAddressView( aJoinRoomByAddressState( eventSink = eventsRecorder, ) ) - val text = rule.activity.getString(R.string.screen_start_chat_join_room_by_address_action) - rule.onNodeWithText(text).performTextInput("#address:matrix.org") + val text = activity!!.getString(R.string.screen_start_chat_join_room_by_address_action) + onNodeWithText(text).performTextInput("#address:matrix.org") eventsRecorder.assertSingle(JoinRoomByAddressEvent.UpdateAddress("#address:matrix.org")) } @Test - fun `clicking on continue emits the expected event`() { + fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setJoinRoomByAddressView( + setJoinRoomByAddressView( aJoinRoomByAddressState( eventSink = eventsRecorder, ) ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle(JoinRoomByAddressEvent.Continue) } } -private fun AndroidComposeTestRule.setJoinRoomByAddressView( +private fun AndroidComposeUiTest.setJoinRoomByAddressView( state: JoinRoomByAddressState, ) { setSafeContent { 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 2bc15e989d..57f6cd2333 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 @@ -17,8 +17,6 @@ import io.element.android.features.startchat.impl.userlist.FakeUserListPresenter import io.element.android.features.startchat.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.startchat.impl.userlist.UserListDataStore import io.element.android.libraries.architecture.AsyncAction -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.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -155,34 +153,16 @@ class StartChatPresenterTest { ) } } - - @Test - fun `present - room directory search`() = runTest { - val presenter = createStartChatPresenter(isRoomDirectorySearchEnabled = true) - presenter.test { - skipItems(1) - awaitItem().let { state -> - assertThat(state.isRoomDirectorySearchEnabled).isTrue() - } - } - } } internal fun createStartChatPresenter( startDMAction: StartDMAction = FakeStartDMAction(), - isRoomDirectorySearchEnabled: Boolean = false, ): StartChatPresenter { - val featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled, - ), - ) return StartChatPresenter( presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()), userRepository = FakeUserRepository(), userListDataStore = UserListDataStore(), startDMAction = startDMAction, - featureFlagService = featureFlagService, buildMeta = aBuildMeta(), ) } diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt index 9237f3433c..8b5e1da8e6 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.startchat.impl.root import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.startchat.impl.R import io.element.android.features.startchat.impl.userlist.aRecentDirectRoomList import io.element.android.features.startchat.impl.userlist.aUserListState @@ -27,70 +29,64 @@ 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.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class StartChatViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class StartChatViewTest : RobolectricTest() { @Test - fun `clicking on back invokes the expected callback`() { + fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onCloseClick = it ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on New room invokes the expected callback`() { + fun `clicking on New room invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onNewRoomClick = it ) - rule.clickOn(R.string.screen_create_room_action_create_room) + clickOn(R.string.screen_create_room_action_create_room) } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Invite people invokes the expected callback`() { + fun `clicking on Invite people invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( applicationName = "test", eventSink = eventsRecorder, ), onInviteFriendsClick = it ) - val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test") - rule.onNodeWithText(text).performClick() + val text = activity!!.getString(CommonStrings.action_invite_friends_to_app, "test") + onNodeWithText(text).performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on a user suggestion invokes the expected callback`() { + fun `clicking on a user suggestion invokes the expected callback`() = runAndroidComposeUiTest { val recentDirectRoomList = aRecentDirectRoomList() val firstRoom = recentDirectRoomList[0] val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(firstRoom.roomId) { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( userListState = aUserListState( recentDirectRooms = recentDirectRoomList @@ -99,42 +95,41 @@ class StartChatViewTest { ), onOpenDM = it ) - rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() + onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() } } @Config(qualifiers = "h1024dp") @Test - fun `clicking on Join room by address invokes the expected callback`() { + fun `clicking on Join room by address invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, ), onJoinRoomByAddressClick = it ) - rule.clickOn(R.string.screen_start_chat_join_room_by_address_action) + clickOn(R.string.screen_start_chat_join_room_by_address_action) } } @Test - fun `clicking on room directory invokes the expected callback`() { + fun `clicking on room directory invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setStartChatView( + setStartChatView( aCreateRoomRootState( eventSink = eventsRecorder, - isRoomDirectorySearchEnabled = true ), onRoomDirectorySearchClick = it ) - rule.clickOn(R.string.screen_room_directory_search_title) + clickOn(R.string.screen_room_directory_search_title) } } } -private fun AndroidComposeTestRule.setStartChatView( +private fun AndroidComposeUiTest.setStartChatView( state: StartChatState, onCloseClick: () -> Unit = EnsureNeverCalled(), onNewRoomClick: () -> Unit = EnsureNeverCalled(), diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index aaafbe04be..aff9e3502d 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -20,7 +20,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.impl.root.UserProfileNode @@ -86,7 +86,7 @@ class UserProfileFlowNode( override fun startCall(dmRoomId: RoomId, callIntent: CallIntent) { elementCallEntryPoint.startCall( - CallType.RoomCall( + CallData( sessionId = sessionId, roomId = dmRoomId, isAudioCall = callIntent == CallIntent.AUDIO diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt index ac146a36a7..5ba0280b14 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.modifiers.niceClickable import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text @@ -140,7 +141,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview { UserProfileHeaderSection( avatarUrl = null, userId = UserId("@alice:example.com"), - userName = "Alice", + userName = USER_NAME_ALICE, verificationState = UserProfileVerificationState.VERIFIED, openAvatarPreview = {}, onUserIdClick = {}, @@ -154,7 +155,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen UserProfileHeaderSection( avatarUrl = null, userId = UserId("@alice:example.com"), - userName = "Alice", + userName = USER_NAME_ALICE, verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION, openAvatarPreview = {}, onUserIdClick = {}, diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt index c3caffa7f3..62efbf1f0f 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt @@ -84,7 +84,6 @@ private fun PreferenceBlockUser( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), onClick = { if (!isLoading) eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true)) }, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, - style = ListItemStyle.Primary, ) } else { ListItem( diff --git a/features/userprofile/shared/src/main/res/values-ca/translations.xml b/features/userprofile/shared/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..bacb920a58 --- /dev/null +++ b/features/userprofile/shared/src/main/res/values-ca/translations.xml @@ -0,0 +1,19 @@ + + + "Bloqueja" + "Els usuaris bloquejats no podran enviar-te missatges i tots els seus missatges s\'amagaran. Pots desbloquejar-los en qualsevol moment." + "Bloqueja usuari" + "Desbloqueja" + "Podràs tornar a veure tots els seus missatges." + "Desbloqueja usuari" + "Bloqueja" + "Els usuaris bloquejats no podran enviar-te missatges i tots els seus missatges s\'amagaran. Pots desbloquejar-los en qualsevol moment." + "Bloqueja usuari" + "Perfil" + "Desbloqueja" + "Podràs tornar a veure tots els seus missatges." + "Desbloqueja usuari" + "Utilitza l\'aplicació web per verificar aquest usuari." + "Verifica %1$s" + "S\'ha produït un error en intentar iniciar un xat" + diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt index 83b10e2a53..63930affee 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.userprofile import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState @@ -39,193 +41,187 @@ import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithTwoParams import io.element.android.tests.testutils.pressBack -import kotlinx.coroutines.test.runTest -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class UserProfileViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class UserProfileViewTest : RobolectricTest() { @Test - fun `on back button click - the expected callback is called`() = runTest { + fun `on back button click - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setUserProfileView( + setUserProfileView( goBack = callback, ) - rule.pressBack() + pressBack() } } @Test - fun `on avatar clicked - the expected callback is called`() = runTest { + fun `on avatar clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL), openAvatarPreview = callback, ) - rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() + onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() } } @Test - fun `on avatar clicked with no avatar - nothing happens`() = runTest { + fun `on avatar clicked with no avatar - nothing happens`() = runAndroidComposeUiTest { val callback = EnsureNeverCalledWithTwoParams() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null), openAvatarPreview = callback, ) - rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() + onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() } @Test - fun `on Share clicked - the expected callback is called`() = runTest { + fun `on Share clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setUserProfileView( + setUserProfileView( onShareUser = callback, ) - rule.clickOn(CommonStrings.action_share) + clickOn(CommonStrings.action_share) } } @Test - fun `on Message clicked - the StartDm event is emitted`() = runTest { + fun `on Message clicked - the StartDm event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_message) + clickOn(CommonStrings.action_message) eventsRecorder.assertSingle(UserProfileEvents.StartDM) } @Test - fun `on Call clicked - the expected callback is called`() = runTest { + fun `on Call clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.AUDIO) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, canCall = true, ), onStartCall = callback, ) - rule.clickOn(CommonStrings.action_call) + clickOn(CommonStrings.action_call) } } @Test - fun `on Video Call clicked - the expected callback is called`() = runTest { + fun `on Video Call clicked - the expected callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithTwoParams(A_ROOM_ID, CallIntent.VIDEO) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, canCall = true, ), onStartCall = callback, ) - rule.clickOn(CommonStrings.common_video) + clickOn(CommonStrings.common_video) } } @Config(qualifiers = "h1024dp") @Test - fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest { + fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_block_user) + clickOn(R.string.screen_dm_details_block_user) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true)) } @Test - fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest { + fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_block_alert_action) + clickOn(R.string.screen_dm_details_block_alert_action) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false)) } @Test - fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest { + fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Config(qualifiers = "h1024dp") @Test - fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest { + fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_unblock_user) + clickOn(R.string.screen_dm_details_unblock_user) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true)) } @Test - fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest { + fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, eventSink = eventsRecorder, ), ) - rule.clickOn(R.string.screen_dm_details_unblock_alert_action) + clickOn(R.string.screen_dm_details_unblock_alert_action) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false)) } @Test - fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest { + fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, eventSink = eventsRecorder, ), ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Test - fun `on verify user clicked - the right callback is called`() = runTest { + fun `on verify user clicked - the right callback is called`() = runAndroidComposeUiTest { ensureCalledOnceWithParam(A_USER_ID) { callback -> - rule.setUserProfileView( + setUserProfileView( state = aUserProfileState(userId = A_USER_ID, verificationState = UserProfileVerificationState.UNVERIFIED), onVerifyClick = callback, ) - rule.clickOn(CommonStrings.common_verify_user) + clickOn(CommonStrings.common_verify_user) } } } -private fun AndroidComposeTestRule.setUserProfileView( +private fun AndroidComposeUiTest.setUserProfileView( state: UserProfileState = aUserProfileState( eventSink = EventsRecorder(expectEvents = false), ), diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt index 3219658796..b0a2590e8a 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt @@ -6,11 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.userprofile.shared.blockuser -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.userprofile.api.UserProfileEvents import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.shared.R @@ -18,18 +19,14 @@ import io.element.android.features.userprofile.shared.aUserProfileState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class BlockUserDialogsTest { - @get:Rule val rule = createAndroidComposeRule() +class BlockUserDialogsTest : RobolectricTest() { @Test - fun `confirm block user emit expected Event`() { + fun `confirm block user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -37,14 +34,14 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(R.string.screen_dm_details_block_alert_action) + clickOn(R.string.screen_dm_details_block_alert_action) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false)) } @Test - fun `cancel block user emit expected Event`() { + fun `cancel block user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -52,14 +49,14 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } @Test - fun `confirm unblock user emit expected Event`() { + fun `confirm unblock user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, @@ -67,14 +64,14 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(R.string.screen_dm_details_unblock_alert_action) + clickOn(R.string.screen_dm_details_unblock_alert_action) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false)) } @Test - fun `cancel unblock user emit expected Event`() { + fun `cancel unblock user emit expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setContent { + setContent { BlockUserDialogs( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, @@ -82,7 +79,7 @@ class BlockUserDialogsTest { ) ) } - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt index 77f5af4a2e..895f78cbdc 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.FlowId import io.element.android.libraries.matrix.api.core.UserId @@ -59,7 +60,7 @@ internal fun anIncomingSessionVerificationRequest() = VerificationRequest.Incomi details = SessionVerificationRequestDetails( senderProfile = MatrixUser( userId = UserId("@alice:example.com"), - displayName = "Alice", + displayName = USER_NAME_ALICE, avatarUrl = null, ), flowId = FlowId("1234"), @@ -73,7 +74,7 @@ internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming. details = SessionVerificationRequestDetails( senderProfile = MatrixUser( userId = UserId("@alice:example.com"), - displayName = "Alice", + displayName = USER_NAME_ALICE, avatarUrl = null, ), flowId = FlowId("1234"), diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt index 7932ccc484..465fc20066 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateMachine.kt @@ -46,7 +46,7 @@ class OutgoingVerificationStateMachine( inState { onEnterEffect { event -> when (event.verificationRequest) { - is VerificationRequest.Outgoing.CurrentSession -> sessionVerificationService.requestCurrentSessionVerification() + is VerificationRequest.Outgoing.CurrentSession -> sessionVerificationService.requestDeviceVerification() is VerificationRequest.Outgoing.User -> sessionVerificationService.requestUserVerification(event.verificationRequest.userId) } } @@ -56,7 +56,7 @@ class OutgoingVerificationStateMachine( } inState { onEnterEffect { - sessionVerificationService.startVerification() + sessionVerificationService.startSasVerification() } } inState { diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt index 2dd2850174..1c199f8826 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt @@ -24,8 +24,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.focused +import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -227,7 +229,11 @@ private fun ContentInitial( Text( modifier = Modifier .clickable { onLearnMoreClick() } - .padding(vertical = 4.dp, horizontal = 16.dp), + .padding(vertical = 4.dp, horizontal = 16.dp) + .semantics { + // Note: there is no Role.Link, so we use Role.Button for better accessibility support + role = Role.Button + }, text = stringResource(CommonStrings.action_learn_more), style = ElementTheme.typography.fontBodyLgMedium ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt index edaadc583d..c398d72ddd 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.core.UserId @@ -86,7 +87,7 @@ internal fun VerificationUserProfileContentPreview() = ElementPreview( VerificationUserProfileContent( user = MatrixUser( userId = UserId("@alice:example.com"), - displayName = "Alice", + displayName = USER_NAME_ALICE, avatarUrl = "https://example.com/avatar.png", ) ) diff --git a/features/verifysession/impl/src/main/res/values-be/translations.xml b/features/verifysession/impl/src/main/res/values-be/translations.xml index a11cb7d607..375274c05e 100644 --- a/features/verifysession/impl/src/main/res/values-be/translations.xml +++ b/features/verifysession/impl/src/main/res/values-be/translations.xml @@ -11,11 +11,11 @@ "Выкарыстоўвайце іншую прыладу" "Чаканне на іншай прыладзе…" "Здаецца, нешта не так. Альбо час чакання запыту скончыўся, альбо запыт быў адхілены." - "Пераканайцеся, што прыведзеныя ніжэй эмодзі супадаюць з эмодзі, паказанымі ў вашым іншым сеансе." + "Пераканайцеся, што эмодзі ніжэй супадаюць з тымі, што паказаны на вашай іншай прыладзе." "Параўнайце эмодзі" "Пераканайцеся, што прыведзеныя ніжэй лічбы супадаюць з лічбамі, паказанымі ў іншым сеансе." "Параўнайце лічбы" - "Ваш новы сеанс пацверджаны. Ён мае доступ да вашых зашыфраваных паведамленняў, і іншыя карыстальнікі будуць лічыць яго давераным." + "Цяпер вы можаце бяспечна чытаць або адпраўляць паведамленні на іншай прыладзе." "Прылада праверана" "Увядзіце ключ аднаўлення" "Дакажыце, што гэта вы, каб атрымаць доступ да вашай зашыфраванай гісторыі паведамленняў." @@ -25,11 +25,11 @@ "Чаканне супадзення" "Параўнайце ўнікальны набор эмодзі." "Параўнайце ўнікальныя эмодзі, пераканаўшыся, што яны размешчаны ў тым жа парадку." - "Ваш новы сеанс пацверджаны. Ён мае доступ да вашых зашыфраваных паведамленняў, і іншыя карыстальнікі будуць лічыць яго давераным." + "Цяпер вы можаце бяспечна чытаць або адпраўляць паведамленні на іншай прыладзе." "Прылада праверана" "Яны не супадаюць" "Яны супадаюць" "Для працягу працы прыміце запыт на запуск працэсу праверкі ў іншым сеансе." "Чаканне прыняцця запыту" - "Выхад…" + "Выдаленне прылады…" diff --git a/features/verifysession/impl/src/main/res/values-ca/translations.xml b/features/verifysession/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..d5ca8fb167 --- /dev/null +++ b/features/verifysession/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,54 @@ + + + "No pots confirmar-la?" + "Crea nova clau de recuperació" + "Verifica aquest dispositiu per configurar missatges segurs." + "Confirma la teva identitat" + "Utilitza un altre dispositiu" + "Utilitza clau de recuperació" + "Ara pots llegir o enviar missatges de manera segura, i qualsevol persona amb qui xategis també confiarà en aquest dispositiu." + "Dispositiu verificat" + "Utilitza un altre dispositiu" + "Esperant un altre dispositiu…" + "Alguna cosa no ha anat bé. O bé s\'ha superat el temps màxim d\'espera de la sol·licitud o bé s\'ha denegat." + "Verifica que les emoticones següents coincideixen amb les que es mostren a l\'altre dispositiu." + "Compara emoticones" + "Verifica que les emoticones següents coincideixen amb les que es mostren al dispositiu de l\'altre usuari." + "Comprova que els números següents coincideixen amb els mostrats a l\'altra sessió." + "Compara números" + "Ara ja pots enviar i llegir missatges de manera segura al teu altre dispositiu." + "Ara pots confiar en la identitat d\'aquest usuari quan enviïs o rebis missatges." + "Dispositiu verificat" + "Introdueix clau de recuperació" + "S\'ha esgotat el temps d\'espera de la sol·licitud, s\'ha denegat la sol·licitud o ha fallat la verificació." + "Demostra que ets tu per poder accedir a l\'historial de missatges xifrats." + "Obre una sessió existent" + "Torna a intentar verificació" + "Estic a punt" + "Esperant a que coincideixin…" + "Compara un conjunt únic d\'emoticones." + "Compara les emoticones i assegura\'t que apareixen en el mateix ordre." + "Sessió iniciada" + "S\'ha esgotat el temps d\'espera de la sol·licitud, s\'ha denegat la sol·licitud o ha fallat la verificació." + "Error de verificació" + "Només continua si tu has iniciat aquesta verificació." + "Verifica l\'altre dispositiu per mantenir segur l\'historial de missatges." + "Ara ja pots enviar i llegir missatges de manera segura al teu altre dispositiu." + "Dispositiu verificat" + "Verificació sol·licitada" + "No coincideixen" + "Coincideixen" + "Assegura\'t que tens l\'aplicació oberta a l\'altre dispositiu abans d\'inicar la verificació des d\'aquí." + "Obre l\'aplicació en un altre dispositiu verificat" + "Per a més seguretat, verifica aquest usuari comparant un conjunt d\'emoticones vostres dispositius. Fes-ho a través d\'un mitjà de comunicació fiable, o en persona." + "Vols verificar aquest usuari?" + "Per a més seguretat, un altre usuari vol verificar la teva identitat. Se\'t mostrarà un conjunt d\'emoticones a comparar." + "Hauries de veure una finestra emergent a l\'altre dispositiu. Inicia, ara, la verificació des d\'allà." + "Inicia la verificació a l\'altre dispositiu" + "Inicia la verificació a l\'altre dispositiu" + "Esperant a l\'altre usuari" + "Un cop acceptat, podràs continuar amb la verificació." + "Per continuar, accepta la sol·licitud per iniciar el procés de verificació a l\'altra sessió." + "Esperant que s\'accepti la sol·licitud" + "S\'està tancant la sessió…" + diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml index f50879c573..2e46202dd4 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -3,7 +3,7 @@ "Bestätigung unmöglich?" "Erstelle einen neuen Wiederherstellungsschlüssel" "Wähle eine Verifizierungsmethode, um den sicheren Nachrichtenversand einzurichten." - "Bestätige deine Identität" + "Bestätige deine digitale Identität" "Ein anderes Gerät verwenden" "Wiederherstellungsschlüssel verwenden" "Du kannst jetzt verschlüsselte Nachrichten lesen und versenden. Dein Chatpartner vertraut nun diesem Gerät." @@ -17,7 +17,7 @@ "Bestätige, dass die folgenden Zahlen mit denen in deiner anderen Sitzung übereinstimmen." "Vergleiche die Zahlen" "Jetzt kannst du verschlüsselte Nachrichten sicher auf deinem anderen Gerät schreiben und lesen." - "Jetzt kannst du der Identität dieses Nutzers vertrauen, wenn du Nachrichten sendest oder empfängst." + "Jetzt kannst du beim Senden oder Empfangen von Nachrichten der digitalen Identität dieses Nutzers vertrauen." "Gerät verifiziert" "Wiederherstellungsschlüssel eingeben" "Entweder ist die Anfrage abgelaufen, oder die Anfrage wurde abgelehnt, oder es gab eine Unstimmigkeit bei der Überprüfung." @@ -42,7 +42,7 @@ "Öffne die App auf einem anderen verifizierten Gerät" "Verifiziere diesen Nutzer für zusätzliche Sicherheit durch den Vergleich einer Reihe von Emojis auf den Geräten. Verwende dazu einen vertraulichen Kommunikationskanal." "Diesen Nutzer verifizieren?" - "Für zusätzliche Sicherheit möchte ein anderer Nutzer deine Identität verifizieren. Es werden dir einige Emojis zum Vergleich angezeigt." + "Zur zusätzlichen Sicherheit möchte ein anderer Nutzer deine digitale Identität verifizieren. Dir werden einige Emojis zum Abgleich angezeigt." "Du solltest ein Popup-Fenster auf dem anderen Gerät sehen. Starte die Verifizierung von dort aus." "Starte die Verifizierung auf dem anderen Gerät" "Starte die Verifizierung auf dem anderen Gerät" diff --git a/features/verifysession/impl/src/main/res/values-et/translations.xml b/features/verifysession/impl/src/main/res/values-et/translations.xml index f09202c9c2..25cf6272fe 100644 --- a/features/verifysession/impl/src/main/res/values-et/translations.xml +++ b/features/verifysession/impl/src/main/res/values-et/translations.xml @@ -2,7 +2,7 @@ "Kas kinnitamine pole võimalik?" "Loo uus taastevõti" - "Krüptitud sõnumivahetuse tagamiseks verifitseeri see seade." + "Turvalise sõnumside seadistamiseks vali verifitseerimise viis." "Kinnita oma digitaalne identiteet" "Kasuta teist seadet" "Kasuta taastevõtit" @@ -17,7 +17,7 @@ "Kinnita, et kõik järgnevalt kuvatud numbrid on täpselt samad, mida sa näed oma teises sessioonis." "Võrdle numbreid" "Võid nüüd sõnumeid oma teises seadmes turvaliselt saata ja vastu võtta." - "Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja identiteeti usaldada." + "Nüüd sa võid sõnumite vastuvõtmisel ja saatmisel selle kasutaja digitaalset identiteeti usaldada." "Seade on verifitseeritud" "Sisesta taastevõti" "Kas verifitseerimine aegus, teine osapool keeldus vastamast või tekkis vastuste mittevastavus." @@ -42,7 +42,7 @@ "Ava rakendus teises verifitseeritud seadmes" "Lisaturvalisuse nimel verifitseeri seee kasutaja, võrreldes oma seadmetes olevaid emojisid. Tee seda, kasutades usaldusväärset suhtlusviisi." "Kas verifitseerime selle kasutaja?" - "Lisaturvalisuse nimel soovib teine kasutaja sinu identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema." + "Lisaturvalisuse nimel soovib teine kasutaja sinu digitaalse identiteeti verifitseerida. Järgmiseks näed sa emojisid, mida peate omavahel võrdlema." "Sa peaksid teises seadmes nägema hüpikakent. Palun alusta sealt verifitseerimist." "Alusta verifitseerimist teises seadmes" "Alusta verifitseerimist teises seadmes" @@ -50,5 +50,5 @@ "Kui oled nõustunud, siis saad sa verifitseerimist jätkata." "Jätkamaks nõustu verifitseerimisprotsessi alustamisega oma teises sessioonis." "Ootame nõustumist verifitseerimispäringuga" - "Logime välja…" + "Eemaldan seadet…" diff --git a/features/verifysession/impl/src/main/res/values-fa/translations.xml b/features/verifysession/impl/src/main/res/values-fa/translations.xml index ffc03e1f19..3447c07a7d 100644 --- a/features/verifysession/impl/src/main/res/values-fa/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fa/translations.xml @@ -13,6 +13,7 @@ "يه چيزي درست به نظر نمياد یا زمان درخواست به پایان رسید یا درخواست رد شد." "تأیید تطابق شکلک‌های زیر با شکلک‌های نشان داده شده روی افزارهٔ دیگرتان." "مقایسهٔ شکلک‌ها" + "تأیید کنید که اعداد زیر با اعداد نشان داده شده در جلسه دیگر شما مطابقت دارند." "مقایسهٔ اعداد" "اکنون می‌توانید روی افزارهٔ دیگرتان با امنیت پیام فرستاده و بخوانید." "افزاره تأیید شده" @@ -32,5 +33,5 @@ "مطابقند" "برای ادامه، درخواست شروع فرآیند تأیید را در جلسه دیگر خود بپذیرید." "منظر پذیرش درخواست" - "خارج شدن…" + "برداشتن افزاره…" diff --git a/features/verifysession/impl/src/main/res/values-in/translations.xml b/features/verifysession/impl/src/main/res/values-in/translations.xml index 021a528646..05a5bae0ab 100644 --- a/features/verifysession/impl/src/main/res/values-in/translations.xml +++ b/features/verifysession/impl/src/main/res/values-in/translations.xml @@ -50,5 +50,5 @@ "Setelah diterima, Anda akan dapat melanjutkan verifikasi." "Terima permintaan untuk memulai proses verifikasi di sesi Anda yang lain untuk melanjutkan." "Menunggu untuk menerima permintaan" - "Mengeluarkan dari akun…" + "Mengeluarkan device dari akun…" diff --git a/features/verifysession/impl/src/main/res/values-pl/translations.xml b/features/verifysession/impl/src/main/res/values-pl/translations.xml index 92b0572d08..39ce9f6fa3 100644 --- a/features/verifysession/impl/src/main/res/values-pl/translations.xml +++ b/features/verifysession/impl/src/main/res/values-pl/translations.xml @@ -2,8 +2,8 @@ "Nie możesz potwierdzić?" "Utwórz nowy klucz przywracania" - "Zweryfikuj to urządzenie, aby skonfigurować bezpieczne przesyłanie wiadomości." - "Potwierdź, że to Ty" + "Wybierz sposób weryfikacji, aby skonfigurować bezpieczne wiadomości." + "Potwierdź swoją tożsamość cyfrową" "Użyj innego urządzenia" "Użyj klucza przywracania" "Teraz możesz bezpiecznie czytać i wysyłać wiadomości, każdy z kim czatujesz również może ufać temu urządzeniu." @@ -50,5 +50,5 @@ "Po zaakceptowaniu będziesz mógł kontynuować weryfikację." "Zaakceptuj prośbę o rozpoczęcie procesu weryfikacji w innej sesji, aby kontynuować." "Oczekiwanie na zaakceptowanie prośby" - "Wylogowywanie…" + "Usuwam urządzenie…" diff --git a/features/verifysession/impl/src/main/res/values-pt/translations.xml b/features/verifysession/impl/src/main/res/values-pt/translations.xml index bf32d3c7b9..2bbda1497a 100644 --- a/features/verifysession/impl/src/main/res/values-pt/translations.xml +++ b/features/verifysession/impl/src/main/res/values-pt/translations.xml @@ -3,7 +3,7 @@ "Não é possível confirmar?" "Criar uma nova chave de recuperação" "Verifica este dispositivo para configurar o envio seguro de mensagens." - "Confirma que és tu" + "Confirma a tua identidade digital" "Utilizar outro dispositivo" "Utilizar chave de recuperação" "Agora podes ler ou enviar mensagens de forma segura, e qualquer pessoa com quem converses também pode confiar neste dispositivo." @@ -50,5 +50,5 @@ "Uma vez aceite, poderás continuar com a verificação." "Para continuar, aceita o pedido de verificação na tua outra sessão." "À aguardar a aceitação do pedido" - "A terminar sessão…" + "A remover dispositivo…" diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index cb22fd02d9..25f897d954 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -17,7 +17,7 @@ "Confirmați că numerele de mai jos se potrivesc cu cele afișate în cealaltă sesiune." "Comparați numerele" "Noua dumneavoastră sesiune este acum verificată. Are acces la mesajele dumneavoastră criptate, iar ceilalti utilizatori vă vor vedea ca fiind de încredere." - "Acum puteți avea încredere în identitatea acestui utilizator atunci când trimiteți sau primiți mesaje." + "Acum puteți avea încredere în identitatea digitală a acestui utilizator atunci când trimiteți sau primiți mesaje." "Dispozitiv verificat" "Introduceți cheia de recuperare" "Fie cererea a expirat, cererea a fost respinsă, fie a existat o nepotrivire de verificare." @@ -42,7 +42,7 @@ "Deschideți aplicația pe un alt dispozitiv verificat" "Pentru securitate suplimentară, verificați acest utilizator comparând un set de emoji-uri pe dispozitivele dvs. Faceți acest lucru utilizând o metodă de comunicare de încredere." "Verificați acest utilizator?" - "Pentru o securitate suplimentară, un alt utilizator dorește să vă verifice identitatea. Vi se va afișa un set de emoji-uri pentru comparație." + "Pentru o securitate sporită, un alt utilizator dorește să vă verifice identitatea digitală. Vi se va afișa un set de emoji-uri pentru comparație." "Ar trebui să vedeți o fereastră pop-up pe celălalt dispozitiv. Începeți verificarea de acolo acum." "Începeți verificarea pe celălalt dispozitiv" "Începeți verificarea pe celălalt dispozitiv" 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 91974caa1f..d90cd71386 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -6,7 +6,7 @@ "Подтвердите личность" "Использовать другое устройство" "Использовать ключ восстановления" - "Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству." + "Теперь Вы можете безопасно читать и отправлять сообщения, и все, с кем Вы общаетесь в чате, также могут доверять этому устройству." "Устройство проверено" "Использовать другое устройство" "Ожидание другого устройства…" diff --git a/features/verifysession/impl/src/main/res/values-sk/translations.xml b/features/verifysession/impl/src/main/res/values-sk/translations.xml index 932b8555fc..d4e6f60402 100644 --- a/features/verifysession/impl/src/main/res/values-sk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sk/translations.xml @@ -50,5 +50,5 @@ "Po prijatí budete môcť pokračovať v overovaní." "Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii." "Čaká sa na prijatie žiadosti" - "Prebieha odhlasovanie…" + "Odoberanie zariadenia…" diff --git a/features/verifysession/impl/src/main/res/values-uk/translations.xml b/features/verifysession/impl/src/main/res/values-uk/translations.xml index 1967fef383..03fe318d13 100644 --- a/features/verifysession/impl/src/main/res/values-uk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-uk/translations.xml @@ -50,5 +50,5 @@ "Після погодження ви зможете продовжити верифікацію." "Щоб продовжити, прийміть запит на початок процесу верифікації в іншому сеансі." "Очікування на прийняття запиту" - "Вихід…" + "Видалення пристрою…" 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 63cb67330b..61b7b078dd 100644 --- a/features/verifysession/impl/src/main/res/values-zh/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh/translations.xml @@ -9,7 +9,7 @@ "现在你可以安全地读取或发送消息,并且与你聊天的任何人也可以信任此设备。" "设备已验证" "使用其它设备" - "正在等待其它设备……" + "正在等待其它设备…" "发生了一些错误。网络请求超时,或者被服务器拒绝。" "确认以下 Emoji 与你的其它设备上显示的相匹配。" "比较 Emoji" @@ -25,7 +25,7 @@ "打开已有会话" "重试验证" "准备就绪" - "等待比对……" + "正在等待对比…" "比较一组唯一的 Emoji。" "比较唯一的 Emoji,确保它们以相同顺序排列。" "已登录" @@ -50,5 +50,5 @@ "一旦被接受,你将能够继续进行验证。" "接受此请求以在另一会话中开始验证流程以继续操作。" "等待接受请求" - "正在删除设备……" + "正在移除设备…" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index c9b9e25b74..a8d3802268 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -99,6 +99,7 @@ class IncomingVerificationPresenterTest { emojiState.eventSink(IncomingVerificationViewEvents.ConfirmVerification) val emojiWaitingItem = awaitItem() assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue() + advanceUntilIdle() approveVerificationLambda.assertions().isCalledOnce() // Remote confirm that the emojis match fakeSessionVerificationService.emitVerificationFlowState( @@ -161,6 +162,7 @@ class IncomingVerificationPresenterTest { emojiState.eventSink(IncomingVerificationViewEvents.DeclineVerification) val emojiWaitingItem = awaitItem() assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue() + advanceUntilIdle() declineVerificationLambda.assertions().isCalledOnce() // Remote confirm that there is a failure fakeSessionVerificationService.emitVerificationFlowState( @@ -260,6 +262,7 @@ class IncomingVerificationPresenterTest { emojiState.eventSink(IncomingVerificationViewEvents.GoBack) val emojiWaitingItem = awaitItem() assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue() + advanceUntilIdle() declineVerificationLambda.assertions().isCalledOnce() // Remote confirm that there is a failure fakeSessionVerificationService.emitVerificationFlowState( diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt index 4aa63f3ab8..5947d1d0a5 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt @@ -6,71 +6,68 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.verifysession.impl.incoming 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class IncomingVerificationViewTest { - @get:Rule val rule = createAndroidComposeRule() +class IncomingVerificationViewTest : RobolectricTest() { // region step Initial @Test - fun `back key pressed - ignore the verification`() { + fun `back key pressed - ignore the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `ignore incoming verification emits the expected event`() { + fun `ignore incoming verification emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_ignore) + clickOn(CommonStrings.action_ignore) eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification) } @Test - fun `start incoming verification emits the expected event`() { + fun `start incoming verification emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_start_verification) + clickOn(CommonStrings.action_start_verification) eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification) } @Test - fun `back key pressed - when awaiting response cancels the verification`() { + fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial( isWaiting = true, @@ -78,16 +75,16 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } // endregion step Initial // region step Verifying @Test - fun `back key pressed - when ready to verify cancels the verification`() { + fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -96,14 +93,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `back key pressed - when verifying and loading emits the expected event`() { + fun `back key pressed - when verifying and loading emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -112,14 +109,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `clicking on they do not match emits the expected event`() { + fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -128,14 +125,14 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_dont_match) + clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification) } @Test - fun `clicking on they match emits the expected event`() { + fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -144,35 +141,35 @@ class IncomingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_match) + clickOn(R.string.screen_session_verification_they_match) eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification) } // endregion // region step Failure @Test - fun `back key pressed - when failure resets the flow`() { + fun `back key pressed - when failure resets the flow`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `click on done - when failure resets the flow`() { + fun `click on done - when failure resets the flow`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_done) + clickOn(CommonStrings.action_done) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @@ -180,33 +177,33 @@ class IncomingVerificationViewTest { // region step Completed @Test - fun `back key pressed - on Completed step emits the expected event`() { + fun `back key pressed - on Completed step emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } @Test - fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() { + fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setIncomingVerificationView( + setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_done) + clickOn(CommonStrings.action_done) eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) } // endregion - private fun AndroidComposeTestRule.setIncomingVerificationView( + private fun AndroidComposeUiTest.setIncomingVerificationView( state: IncomingVerificationState, ) { setContent { diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt index f02f24c9ef..c6cbfca281 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt @@ -6,6 +6,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:Suppress("UnusedImports") + package io.element.android.features.verifysession.impl.outgoing import app.cash.turbine.ReceiveTurbine @@ -27,6 +29,8 @@ import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test 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 @@ -50,11 +54,11 @@ class OutgoingVerificationPresenterTest { @Test fun `present - Handles requestVerification for session verification`() = runTest { - val requestSessionVerificationRecorder = lambdaRecorder {} - val startVerificationRecorder = lambdaRecorder {} + val requestDeviceVerificationRecorder = lambdaRecorder {} + val startSasVerificationRecorder = lambdaRecorder {} val service = unverifiedSessionService( - requestSessionVerificationLambda = requestSessionVerificationRecorder, - startVerificationLambda = startVerificationRecorder, + requestDeviceVerificationLambda = requestDeviceVerificationRecorder, + startSasVerificationLambda = startSasVerificationRecorder, ) val presenter = createOutgoingVerificationPresenter( service = service, @@ -63,18 +67,18 @@ class OutgoingVerificationPresenterTest { presenter.test { requestVerificationAndAwaitVerifyingState(service) - requestSessionVerificationRecorder.assertions().isCalledOnce() - startVerificationRecorder.assertions().isCalledOnce() + requestDeviceVerificationRecorder.assertions().isCalledOnce() + startSasVerificationRecorder.assertions().isCalledOnce() } } @Test fun `present - Handles requestVerification for user verification`() = runTest { val requestUserVerificationRecorder = lambdaRecorder {} - val startVerificationRecorder = lambdaRecorder {} + val startSasVerificationRecorder = lambdaRecorder {} val service = unverifiedSessionService( requestUserVerificationLambda = requestUserVerificationRecorder, - startVerificationLambda = startVerificationRecorder, + startSasVerificationLambda = startSasVerificationRecorder, ) val presenter = createOutgoingVerificationPresenter( service = service, @@ -84,7 +88,7 @@ class OutgoingVerificationPresenterTest { requestVerificationAndAwaitVerifyingState(service) requestUserVerificationRecorder.assertions().isCalledOnce() - startVerificationRecorder.assertions().isCalledOnce() + startSasVerificationRecorder.assertions().isCalledOnce() } } @@ -106,8 +110,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - A failure when verifying cancels it`() = runTest { val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, - startVerificationLambda = { }, + requestDeviceVerificationLambda = { }, + startSasVerificationLambda = { }, approveVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -125,7 +129,7 @@ class OutgoingVerificationPresenterTest { @Test fun `present - A fail when requesting verification resets the state to the canceled one`() = runTest { val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, + requestDeviceVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { @@ -139,8 +143,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, - startVerificationLambda = { }, + requestDeviceVerificationLambda = { }, + startSasVerificationLambda = { }, cancelVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -154,8 +158,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, - startVerificationLambda = { }, + requestDeviceVerificationLambda = { }, + startSasVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { @@ -168,8 +172,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - Go back after cancellation returns to initial state`() = runTest { val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, - startVerificationLambda = { }, + requestDeviceVerificationLambda = { }, + startSasVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { @@ -189,8 +193,8 @@ class OutgoingVerificationPresenterTest { VerificationEmoji(number = 30) ) val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, - startVerificationLambda = { }, + requestDeviceVerificationLambda = { }, + startSasVerificationLambda = { }, approveVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -215,8 +219,8 @@ class OutgoingVerificationPresenterTest { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { val service = unverifiedSessionService( - requestSessionVerificationLambda = { }, - startVerificationLambda = { }, + requestDeviceVerificationLambda = { }, + startSasVerificationLambda = { }, declineVerificationLambda = { }, ) val presenter = createOutgoingVerificationPresenter(service) @@ -271,6 +275,7 @@ class OutgoingVerificationPresenterTest { } } + context(testScope: TestScope) private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( fakeService: FakeSessionVerificationService, sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), @@ -278,6 +283,7 @@ class OutgoingVerificationPresenterTest { var state = awaitItem() assertThat(state.step).isEqualTo(Step.Initial) state.eventSink(OutgoingVerificationViewEvents.RequestVerification) + testScope.advanceUntilIdle() // Await for other device response: fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) state = awaitItem() @@ -286,6 +292,7 @@ class OutgoingVerificationPresenterTest { state = awaitItem() assertThat(state.step).isEqualTo(Step.Ready) state.eventSink(OutgoingVerificationViewEvents.StartSasVerification) + testScope.advanceUntilIdle() // Await for other device response (again): fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification) state = awaitItem() @@ -297,30 +304,29 @@ class OutgoingVerificationPresenterTest { return state } - private suspend fun unverifiedSessionService( - requestSessionVerificationLambda: () -> Unit = { lambdaError() }, + private fun unverifiedSessionService( + requestDeviceVerificationLambda: () -> Unit = { lambdaError() }, requestUserVerificationLambda: (UserId) -> Unit = { lambdaError() }, cancelVerificationLambda: () -> Unit = { lambdaError() }, approveVerificationLambda: () -> Unit = { lambdaError() }, declineVerificationLambda: () -> Unit = { lambdaError() }, - startVerificationLambda: () -> Unit = { lambdaError() }, + startSasVerificationLambda: () -> Unit = { lambdaError() }, resetLambda: (Boolean) -> Unit = { }, acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, ): FakeSessionVerificationService { return FakeSessionVerificationService( - requestCurrentSessionVerificationLambda = requestSessionVerificationLambda, + initialSessionVerifiedStatus = SessionVerifiedStatus.NotVerified, + requestDeviceVerificationLambda = requestDeviceVerificationLambda, requestUserVerificationLambda = requestUserVerificationLambda, cancelVerificationLambda = cancelVerificationLambda, approveVerificationLambda = approveVerificationLambda, declineVerificationLambda = declineVerificationLambda, - startVerificationLambda = startVerificationLambda, + startSasVerificationLambda = startSasVerificationLambda, resetLambda = resetLambda, acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda, acceptVerificationRequestLambda = acceptVerificationRequestLambda, - ).apply { - emitVerifiedStatus(SessionVerifiedStatus.NotVerified) - } + ) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt index 71b55fac10..311b9c795e 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt @@ -6,12 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.features.verifysession.impl.outgoing 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 androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.libraries.architecture.AsyncData @@ -21,58 +23,53 @@ 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.pressBackKey -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class OutgoingVerificationViewTest { - @get:Rule val rule = createAndroidComposeRule() +class OutgoingVerificationViewTest : RobolectricTest() { @Test - fun `back key pressed - when canceled resets the flow`() { + fun `back key pressed - when canceled resets the flow`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Canceled, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset) } @Test - fun `back key pressed - when awaiting response cancels the verification`() { + fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) } @Test - fun `back key pressed - when ready to verify cancels the verification`() { + fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Ready, eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) } @Test - fun `back key pressed - when verifying and not loading declines the verification`() { + fun `back key pressed - when verifying and not loading declines the verification`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -81,14 +78,14 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) } @Test - fun `back key pressed - when verifying and loading does nothing`() { + fun `back key pressed - when verifying and loading does nothing`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -97,42 +94,42 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.pressBackKey() + pressBackKey() eventsRecorder.assertEmpty() } @Test - fun `back key pressed - on Completed exits the flow`() { + fun `back key pressed - on Completed exits the flow`() = runAndroidComposeUiTest { ensureCalledOnce { callback -> - rule.setOutgoingVerificationView( + setOutgoingVerificationView( onBack = callback, state = anOutgoingVerificationState( step = OutgoingVerificationState.Step.Completed, ), ) - rule.pressBackKey() + pressBackKey() } } @Test - fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { + fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Completed, eventSink = eventsRecorder ), onFinished = callback, ) - rule.clickOn(CommonStrings.action_continue) + clickOn(CommonStrings.action_continue) } } @Test - fun `clicking on they match emits the expected event`() { + fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -141,14 +138,14 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_match) + clickOn(R.string.screen_session_verification_they_match) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification) } @Test - fun `clicking on they do not match emits the expected event`() { + fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setOutgoingVerificationView( + setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( data = aEmojisSessionVerificationData(), @@ -157,11 +154,11 @@ class OutgoingVerificationViewTest { eventSink = eventsRecorder ), ) - rule.clickOn(R.string.screen_session_verification_they_dont_match) + clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) } - private fun AndroidComposeTestRule.setOutgoingVerificationView( + private fun AndroidComposeUiTest.setOutgoingVerificationView( state: OutgoingVerificationState, onLearnMoreClick: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 53812f1d74..e473ec7ee4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,64 +3,66 @@ [versions] # Project -android_gradle_plugin = "8.13.2" +android_gradle_plugin = "9.2.1" # When updating this, please also update the version in the file ./idea/kotlinc.xml -kotlin = "2.3.20" +kotlin = "2.4.0" kotlinpoet = "2.3.0" -ksp = "2.3.6" -firebaseAppDistribution = "5.2.1" +ksp = "2.3.9" +firebaseAppDistribution = "5.3.0" # AndroidX core = "1.18.0" datastore = "1.2.1" constraintlayout = "2.2.1" constraintlayout_compose = "1.1.1" -lifecycle = "2.10.0" +lifecycle = "2.11.0" activity = "1.13.0" -media3 = "1.10.0" -camera = "1.6.0" +media3 = "1.10.1" +camera = "1.6.1" work = "2.11.2" # Compose -compose_bom = "2026.03.01" +compose_bom = "2026.05.01" # Coroutines -coroutines = "1.10.2" +coroutines = "1.11.0" # Accompanist accompanist = "0.37.3" # Test test_core = "1.7.0" -roborazzi = "1.59.0" +roborazzi = "1.64.0" # Jetbrain -datetime = "0.7.1" +datetime = "0.8.0" serialization_json = "1.11.0" #other -coil = "3.4.0" +coil = "3.5.0" # Rollback to 1.0.4, 1.0.5 has this issue: https://github.com/airbnb/Showkase/issues/420 showkase = "1.0.5" # There is some custom logic in `RootFlowNode` that may break because it reuses some Appyx internal APIs. # When upgrading this version, check state restoration still works fine. appyx = "1.7.1" sqldelight = "2.3.2" -wysiwyg = "2.41.3" +wysiwyg = "2.42.0" telephoto = "0.19.0" haze = "1.7.2" # Dependency analysis -dependencyAnalysis = "3.7.0" +dependencyAnalysis = "3.15.0" # DI -metro = "0.13.2" +metro = "1.2.1" # Auto service autoservice = "1.1.1" # quality detekt = "1.23.8" +# Stick to version < 0.5 since error detection is broken. See https://mrmans0n.github.io/compose-rules/detekt/#supported-versions-matrix +detekt_compose_rules = "0.4.28" # See https://github.com/pinterest/ktlint/releases/ ktlint = "1.8.0" androidx-test-ext-junit = "1.3.0" @@ -72,6 +74,8 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref compose_compiler_plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } # https://developer.android.com/studio/write/java8-support#library-desugaring-versions android_desugar = "com.android.tools:desugar_jdk_libs:2.1.5" +# Used in build.gradle.kts +detekt_compose_rules = { module = "io.nlopez.compose.rules:detekt", version.ref = "detekt_compose_rules" } kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } metro_gradle_plugin = { module = "dev.zacsweers.metro:gradle-plugin", version.ref = "metro" } kotlin_compiler = { module = "org.jetbrains.kotlin:kotlin-compiler", version.ref = "kotlin" } @@ -80,11 +84,12 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:34.12.0" +google_firebase_bom = "com.google.firebase:firebase-bom:34.15.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.21.0" +google_tink = "com.google.crypto.tink:tink-android:1.22.0" +jna = "net.java.dev.jna:jna:5.19.1" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } @@ -98,7 +103,7 @@ androidx_constraintlayout_compose = { module = "androidx.constraintlayout:constr androidx_camera_lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } androidx_camera_view = { module = "androidx.camera:camera-view", version.ref = "camera" } androidx_camera_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } -androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.0.0" +androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.1.0" androidx_workmanager_runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } androidx_recyclerview = "androidx.recyclerview:recyclerview:1.4.0" @@ -111,13 +116,14 @@ androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "medi androidx_media3_transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" } androidx_media3_effect = { module = "androidx.media3:media3-effect", version.ref = "media3" } androidx_media3_common = { module = "androidx.media3:media3-common", version.ref = "media3" } +androidx_media3_exoplayer_midi = { module = "androidx.media3:media3-exoplayer-midi", version.ref = "media3" } androidx_biometric = "androidx.biometric:biometric-ktx:1.4.0-alpha02" androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_startup = "androidx.startup:startup-runtime:1.2.0" androidx_preference = "androidx.preference:preference:1.2.1" -androidx_webkit = "androidx.webkit:webkit:1.15.0" +androidx_webkit = "androidx.webkit:webkit:1.16.0" androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_material3 = { module = "androidx.compose.material3:material3", version = '1.5.0-alpha15' } @@ -142,7 +148,7 @@ accompanist_permission = { module = "com.google.accompanist:accompanist-permissi squareup_seismic = "com.squareup:seismic:1.0.3" # network -network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:5.3.2" +network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:5.4.0" network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" } network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_okhttp = { module = "com.squareup.okhttp3:okhttp" } @@ -161,14 +167,14 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" } test_arch_core = "androidx.arch.core:core-testing:2.2.0" test_junit = "junit:junit:4.13.2" test_runner = "androidx.test:runner:1.7.0" -test_mockk = "io.mockk:mockk:1.14.9" +test_mockk = "io.mockk:mockk:1.14.11" 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.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.2" +test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.9.0" 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 +184,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.04.21" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.06.24" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -188,10 +194,10 @@ coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } -kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0" +kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.5.0" showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" } showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" } -jsoup = "org.jsoup:jsoup:1.21.2" +jsoup = "org.jsoup:jsoup:1.22.2" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.2.0" timber = "com.jakewharton.timber:timber:5.0.1" @@ -200,31 +206,31 @@ 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.14.1" +sqlcipher = "net.zetetic:sqlcipher-android:4.16.0" sqlite = "androidx.sqlite:sqlite-ktx:2.6.2" -unifiedpush = "org.unifiedpush.android:connector:3.3.2" +unifiedpush = "org.unifiedpush.android:connector:3.3.3" vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" 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.2" +maplibre = "org.maplibre.gl:android-sdk:13.3.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" -maplibre_compose = "org.maplibre.compose:maplibre-compose:0.12.1" +maplibre_compose = "org.maplibre.compose:maplibre-compose:0.13.0" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" opusencoder = "io.element.android:opusencoder:1.2.0" zxing_cpp = "io.github.zxing-cpp:android:3.0.2" google_zxing = "com.google.zxing:core:3.5.4" -google_guava = "com.google.guava:guava:33.5.0-android" +google_guava = "com.google.guava:guava:33.6.0-android" haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics -posthog = "com.posthog:posthog-android:3.39.0" -sentry = "io.sentry:sentry-android:8.39.1" +posthog = "com.posthog:posthog-android:3.47.0" +sentry = "io.sentry:sentry-android:8.43.2" # main branch can be tested replacing the version with main-SNAPSHOT -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.33.2" +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.37.0" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.5.3" @@ -234,7 +240,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0" metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } # Element Call -element_call_embedded = "io.element.android:element-call-embedded:0.19.2" +element_call_embedded = "io.element.android:element-call-embedded:0.20.2" # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } @@ -262,13 +268,13 @@ metro = { id = "dev.zacsweers.metro", version.ref = "metro" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = "org.jlleitschuh.gradle.ktlint:14.2.0" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" -dependencycheck = "org.owasp.dependencycheck:12.2.1" +dependencycheck = "org.owasp.dependencycheck:12.2.2" dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } -paparazzi = "app.cash.paparazzi:2.0.0-alpha04" +paparazzi = "app.cash.paparazzi:2.0.0-alpha05" roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" } -sonarqube = "org.sonarqube:7.2.3.7755" +sonarqube = "org.sonarqube:7.3.1.8318" licensee = "app.cash.licensee:1.14.1" compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } gms_google_services = { id = "com.google.gms.google-services", version = "4.4.4" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee3125..61285a659d 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8a848873f3..4bdb2a9ebd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ConsoleMessageLogger.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ConsoleMessageLogger.kt index d165da8640..18d9990c0b 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ConsoleMessageLogger.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ConsoleMessageLogger.kt @@ -33,6 +33,11 @@ class DefaultConsoleMessageLogger : ConsoleMessageLogger { else -> Log.DEBUG } + // Avoid logging any messages that contain "password" to prevent leaking sensitive information + if (consoleMessage.message().contains("password=")) { + return + } + val message = buildString { append(consoleMessage.sourceId()) append(":") @@ -41,20 +46,6 @@ class DefaultConsoleMessageLogger : ConsoleMessageLogger { append(consoleMessage.message()) } - // Avoid logging any messages that contain "password" to prevent leaking sensitive information - if (message.contains("password=")) { - return - } - - Timber.tag(tag).log( - priority = priority, - message = buildString { - append(consoleMessage.sourceId()) - append(":") - append(consoleMessage.lineNumber()) - append(" ") - append(consoleMessage.message()) - }, - ) + Timber.tag(tag).log(priority = priority, message = message) } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/crypto/ClientSecret.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/crypto/ClientSecret.kt new file mode 100644 index 0000000000..d07d302f39 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/crypto/ClientSecret.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.androidutils.crypto + +/** + * Represents a client secret used to encrypt/decrypt data from databases, which can be either a passphrase or a raw key. + */ +sealed interface ClientSecret { + /** + * A passphrase that can be used to derive a key for encryption/decryption. + */ + data class Passphrase(val value: String) : ClientSecret { + override fun formattedAsString(): String = value + } + + /** + * A raw key that can be directly used for encryption/decryption. The key is represented as a byte array, and is formatted as a string in the form of + * `x'...'` where the bytes are encoded as hex characters. + */ + data class RawKey(val bytes: ByteArray) : ClientSecret { + override fun formattedAsString() = "x'${bytes.toHexString()}'" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return bytes.contentEquals((other as RawKey).bytes) + } + + override fun hashCode(): Int { + return bytes.contentHashCode() + } + + override fun toString(): String { + return formattedAsString() + } + } + + /** + * Format the client secret as a string that can be parsed back with [fromString]. + * For a passphrase, this is just the passphrase value. For a raw key, this is the hex-encoded representation of the key formatted as `x'...'`. + */ + fun formattedAsString(): String + + companion object { + /** + * Parse a string representation of a client secret, which can be either a passphrase or a raw key formatted as `x'...'`. + */ + fun fromString(secret: String): ClientSecret { + val regex = Regex("^x'([0-9a-fA-F]+)'$") + val rawKeyMatch = regex.matchEntire(secret) + return if (rawKeyMatch != null) { + RawKey(rawKeyMatch.groupValues[1].hexToByteArray()) + } else { + Passphrase(secret) + } + } + + /** + * Create a [ClientSecret] from raw bytes, which will be treated as a raw key. + */ + fun fromRawBytes(bytes: ByteArray): ClientSecret = when (bytes.size) { + 32 -> RawKey(bytes) + else -> Passphrase(bytes.toHexString()) + } + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/json/JsonProvider.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/json/JsonProvider.kt index 1e25599962..12f2957678 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/json/JsonProvider.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/json/JsonProvider.kt @@ -23,6 +23,13 @@ fun interface JsonProvider { @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) class DefaultJsonProvider : JsonProvider { - private val json: Json by lazy { Json { ignoreUnknownKeys = true } } + private val json: Json by lazy { + Json { + ignoreUnknownKeys = true + allowComments = true + allowTrailingComma = true + } + } + override fun invoke() = json } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DeviceHasVulkanSupport.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DeviceHasVulkanSupport.kt new file mode 100644 index 0000000000..e8e5edcca0 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/DeviceHasVulkanSupport.kt @@ -0,0 +1,31 @@ +/* + * 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.system + +import android.content.Context +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext + +private const val VULKAN_VERSION_1_0 = 0x400003 + +/** + * Checks if the device supports Vulkan 1.0. + * + * This is needed for the location screens that contain maps using MapLibre UI components. + * + * Needed until https://github.com/maplibre/maplibre-native/issues/3079 is resolved and we can automatically choose between OpenGL and Vulkan renderers, + * or no devices support OpenGL anymore. + */ +@Inject +class DeviceHasVulkanSupport( + @ApplicationContext private val context: Context, +) { + operator fun invoke(): Boolean { + return context.packageManager.hasSystemFeature("android.hardware.vulkan.version", VULKAN_VERSION_1_0) + } +} 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 5f19cba136..2d63f12f2e 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 @@ -72,6 +72,7 @@ fun View.showKeyboard(andRequestFocus: Boolean = false) { windowInsetsController?.show(WindowInsets.Type.ime()) } else { val imm = context?.getSystemService() + @Suppress("DEPRECATION") imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } } diff --git a/libraries/androidutils/src/main/res/values-ca/translations.xml b/libraries/androidutils/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..aba099db1f --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ca/translations.xml @@ -0,0 +1,4 @@ + + + "No s\'ha trobat cap aplicació compatible per gestionar aquesta acció." + diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt index a83aa7d69f..6168f1017e 100644 --- a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt @@ -11,14 +11,12 @@ package io.element.android.libraries.androidutils.filesize import android.os.Build import com.google.common.truth.Truth.assertThat import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -@RunWith(RobolectricTestRunner::class) -class AndroidFileSizeFormatterTest { +class AndroidFileSizeFormatterTest : RobolectricTest() { @Config(sdk = [Build.VERSION_CODES.N]) @Test fun `test api 24 long format`() { diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt index 158dd63845..e954d99dfa 100644 --- a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/media/VideoCompressorHelperTest.kt @@ -9,13 +9,11 @@ package io.element.android.libraries.androidutils.media import android.util.Size -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class VideoCompressorHelperTest { +class VideoCompressorHelperTest : RobolectricTest() { @Test fun `test getOutputSize`() { val helper = VideoCompressorHelper(maxSize = 720) diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt index 22928781c9..bb7a272908 100644 --- a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/text/LinkifierHelperTest.kt @@ -16,16 +16,14 @@ import androidx.core.text.inSpans import androidx.core.text.toSpannable import com.google.common.truth.Truth.assertThat import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import org.robolectric.shadow.api.Shadow.newInstanceOf -@RunWith(RobolectricTestRunner::class) -class LinkifierHelperTest { +class LinkifierHelperTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BaseFlowNode.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BaseFlowNode.kt index ce89e8a9d9..da849b753e 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BaseFlowNode.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/BaseFlowNode.kt @@ -11,7 +11,6 @@ package io.element.android.libraries.architecture import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier @@ -88,11 +87,9 @@ inline fun BaseFlowNode.OverlayView( @Composable inline fun BaseFlowNode.BackstackWithOverlayBox( modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit = {}, ) { Box(modifier = modifier) { BackstackView() OverlayView() - content() } } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt index 00e49d7bee..b90e95beca 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/appyx/SafeChildrenTransitionScope.kt @@ -186,6 +186,7 @@ class SafeChildrenTransitionScope( } @OptIn(ExperimentalSharedTransitionApi::class) +@Suppress("ContentSlotReused") @Composable inline fun ParentNode.SafeChildren( navModel: NavModel, diff --git a/libraries/cachestore/api/build.gradle.kts b/libraries/cachestore/api/build.gradle.kts new file mode 100644 index 0000000000..0e03bb5136 --- /dev/null +++ b/libraries/cachestore/api/build.gradle.kts @@ -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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.cachestore.api" +} diff --git a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.kt new file mode 100644 index 0000000000..a448ba7df8 --- /dev/null +++ b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheData.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.cachestore.api + +data class CacheData( + val value: String, + val updatedAt: Long, +) diff --git a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt new file mode 100644 index 0000000000..5df446f688 --- /dev/null +++ b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.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.cachestore.api + +interface CacheStore { + suspend fun storeData(key: String, data: CacheData) + suspend fun getData(key: String): CacheData? + suspend fun deleteData(key: String) + suspend fun deleteAll() +} diff --git a/libraries/cachestore/impl/build.gradle.kts b/libraries/cachestore/impl/build.gradle.kts new file mode 100644 index 0000000000..f0c7ba237c --- /dev/null +++ b/libraries/cachestore/impl/build.gradle.kts @@ -0,0 +1,48 @@ +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") + alias(libs.plugins.sqldelight) +} + +android { + namespace = "io.element.android.libraries.cachestore.impl" +} + +setupDependencyInjection() + +dependencies { + implementation(projects.libraries.androidutils) + implementation(projects.libraries.core) + implementation(projects.libraries.encryptedDb) + api(projects.libraries.cachestore.api) + implementation(libs.sqldelight.driver.android) + implementation(libs.sqlcipher) + implementation(libs.sqlite) + implementation(projects.libraries.di) + implementation(libs.sqldelight.coroutines) + + testCommonDependencies(libs) + testImplementation(libs.sqldelight.driver.jvm) +} + +sqldelight { + databases { + create("CacheDatabase") { + // https://sqldelight.github.io/sqldelight/2.1.0/android_sqlite/migrations/ + // To generate a .db file from your latest schema, run this task + // ./gradlew generateDebugCacheDatabaseSchema + // Test migration by running + // ./gradlew verifySqlDelightMigration + schemaOutputDirectory = File("src/main/sqldelight/databases") + verifyMigrations = true + } + } +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.kt new file mode 100644 index 0000000000..2a33ce4fda --- /dev/null +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/CacheDataMapper.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.libraries.cachestore.impl + +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.CacheData as DbCacheData + +internal fun CacheData.toDbModel(key: String): DbCacheData { + return DbCacheData( + key = key, + value_ = value, + updatedAt = updatedAt, + ) +} + +internal fun DbCacheData.toApiModel(): CacheData { + return CacheData( + value = value_, + updatedAt = updatedAt, + ) +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt new file mode 100644 index 0000000000..54766803f9 --- /dev/null +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/DatabaseCacheStore.kt @@ -0,0 +1,40 @@ +/* + * 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.cachestore.impl + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DatabaseCacheStore( + private val database: CacheDatabase, +) : CacheStore { + override suspend fun getData(key: String): CacheData? { + return database.cacheDataQueries.selectData(key) + .executeAsOneOrNull() + ?.toApiModel() + } + + override suspend fun storeData(key: String, data: CacheData) { + database.cacheDataQueries.insertData( + data.toDbModel(key) + ).await() + } + + override suspend fun deleteData(key: String) { + database.cacheDataQueries.deleteData(key).await() + } + + override suspend fun deleteAll() { + database.cacheDataQueries.deleteAll().await() + } +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt new file mode 100644 index 0000000000..89de82a08f --- /dev/null +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt @@ -0,0 +1,56 @@ +/* + * 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.cachestore.impl.di + +import android.content.Context +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.cachestore.impl.CacheDatabase +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.encrypteddb.SqlCipherDriverFactory +import io.element.encrypteddb.passphrase.RandomDatabaseSecretProvider +import io.element.encrypteddb.utils.ReplaceDatabaseKey +import timber.log.Timber + +@BindingContainer +@ContributesTo(AppScope::class) +object CacheStoreModule { + @Provides + @SingleIn(AppScope::class) + fun provideCacheDatabase( + @ApplicationContext context: Context, + ): CacheDatabase { + val name = "cache_database" + val secretFile = context.getDatabasePath("$name.key") + + // Make sure the parent directory of the key file exists, otherwise it will crash in older Android versions + val parentDir = secretFile.parentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + } + + val rekeyMigrationVersion = 2L + val passphraseProvider = RandomDatabaseSecretProvider(context, secretFile) + val driver = SqlCipherDriverFactory(passphraseProvider) + .create( + schema = CacheDatabase.Schema, + name = "$name.db", + context = context + ) { db, oldVersion, newVersion -> + Timber.d("Migrating $name database from version $oldVersion to $newVersion") + if (rekeyMigrationVersion in oldVersion..newVersion) { + ReplaceDatabaseKey(passphraseProvider).replaceKey(name, db) + } + } + + return CacheDatabase(driver) + } +} diff --git a/libraries/cachestore/impl/src/main/sqldelight/databases/1.db b/libraries/cachestore/impl/src/main/sqldelight/databases/1.db new file mode 100644 index 0000000000..8e4b0cac72 Binary files /dev/null and b/libraries/cachestore/impl/src/main/sqldelight/databases/1.db differ diff --git a/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq new file mode 100644 index 0000000000..fd350ac7ba --- /dev/null +++ b/libraries/cachestore/impl/src/main/sqldelight/io/element/android/libraries/cachestore/CacheData.sq @@ -0,0 +1,28 @@ +-------------------------------------------------------------------- +-- Current version of the DB is the highest value of filename +-- in the folder `sqldelight/databases`. +-- +-- When upgrading the schema, you have to create a file .sqm in the +-- `sqldelight/databases` folder and run the following task to +-- generate a .db file using the latest schema +-- > ./gradlew generateDebugCacheDatabaseSchema +-------------------------------------------------------------------- + +CREATE TABLE CacheData ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + updatedAt INTEGER NOT NULL +); + + +selectData: +SELECT * FROM CacheData WHERE key = ?; + +insertData: +INSERT OR REPLACE INTO CacheData VALUES ?; + +deleteData: +DELETE FROM CacheData WHERE key = ?; + +deleteAll: +DELETE FROM CacheData; diff --git a/libraries/cachestore/impl/src/main/sqldelight/migrations/1.sqm b/libraries/cachestore/impl/src/main/sqldelight/migrations/1.sqm new file mode 100644 index 0000000000..f038b85649 --- /dev/null +++ b/libraries/cachestore/impl/src/main/sqldelight/migrations/1.sqm @@ -0,0 +1,8 @@ +-- Migrate DB from version 1 +-- Initial schema for CacheData table + +CREATE TABLE IF NOT EXISTS CacheData ( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + updatedAt INTEGER NOT NULL +); diff --git a/libraries/cachestore/impl/src/main/sqldelight/migrations/2.sqm b/libraries/cachestore/impl/src/main/sqldelight/migrations/2.sqm new file mode 100644 index 0000000000..e7adfb091a --- /dev/null +++ b/libraries/cachestore/impl/src/main/sqldelight/migrations/2.sqm @@ -0,0 +1,2 @@ +-- Migrate DB from version 2 +-- Dummy migration to trigger a re-key diff --git a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt new file mode 100644 index 0000000000..36d7e05532 --- /dev/null +++ b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseCacheStoreTest.kt @@ -0,0 +1,86 @@ +/* + * 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.sessionstorage.impl + +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.impl.CacheDatabase +import io.element.android.libraries.cachestore.impl.DatabaseCacheStore +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import io.element.android.libraries.cachestore.CacheData as DbCacheData + +private const val A_KEY = "aKey" +private const val A_DATA_1 = "aData1" +private const val A_DATA_2 = "aData2" + +class DatabaseCacheStoreTest { + private lateinit var database: CacheDatabase + private lateinit var databaseCacheStore: DatabaseCacheStore + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + // Initialise in memory SQLite driver + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + CacheDatabase.Schema.create(driver) + + database = CacheDatabase(driver) + databaseCacheStore = DatabaseCacheStore( + database = database, + ) + } + + @Test + fun `storeData persists the CacheData into the DB, deleteData deletes it`() = runTest { + // Assert that no data is stored for the key + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + // Store data + databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_1, 1)) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo( + DbCacheData( + key = A_KEY, + value_ = A_DATA_1, + updatedAt = 1, + ) + ) + // Update data + databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_2, 2)) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo( + DbCacheData( + key = A_KEY, + value_ = A_DATA_2, + updatedAt = 2, + ) + ) + // Delete data + databaseCacheStore.deleteData(A_KEY) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + } + + @Test + fun `deleteAll deletes all the data`() = runTest { + // Assert that no data is stored for the key + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + // Store data + databaseCacheStore.storeData(A_KEY, CacheData(A_DATA_1, 1)) + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isEqualTo( + DbCacheData( + key = A_KEY, + value_ = A_DATA_1, + updatedAt = 1, + ) + ) + // Delete all data + databaseCacheStore.deleteAll() + assertThat(database.cacheDataQueries.selectData(A_KEY).executeAsOneOrNull()).isNull() + } +} diff --git a/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt new file mode 100644 index 0000000000..3dca9efaf3 --- /dev/null +++ b/libraries/cachestore/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.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.sessionstorage.impl + +import io.element.android.libraries.cachestore.CacheData +import java.util.Date + +internal fun aCacheData( + key: String = "aKey", + value: String = "aValue", + updatedAt: Date = Date(), +) = CacheData( + key = key, + value_ = value, + updatedAt = updatedAt.time, +) diff --git a/libraries/cachestore/test/build.gradle.kts b/libraries/cachestore/test/build.gradle.kts new file mode 100644 index 0000000000..7ad2ac48d9 --- /dev/null +++ b/libraries/cachestore/test/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.cachestore.test" +} + +dependencies { + implementation(projects.libraries.cachestore.api) +} diff --git a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.kt new file mode 100644 index 0000000000..30633e8ff9 --- /dev/null +++ b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/CacheData.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.sessionstorage.test + +import io.element.android.libraries.cachestore.api.CacheData + +fun aCacheData( + value: String = "aValue", + updatedAt: Long = 0, +) = CacheData( + value = value, + updatedAt = updatedAt, +) diff --git a/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.kt new file mode 100644 index 0000000000..f029c8bcde --- /dev/null +++ b/libraries/cachestore/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemoryCacheStore.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.sessionstorage.test + +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore + +class InMemoryCacheStore( + initialData: Map = emptyMap(), +) : CacheStore { + val dataMap = initialData.toMutableMap() + + override suspend fun storeData(key: String, data: CacheData) { + dataMap[key] = data + } + + override suspend fun getData(key: String): CacheData? { + return dataMap[key] + } + + override suspend fun deleteData(key: String) { + dataMap.remove(key) + } + + override suspend fun deleteAll() { + dataMap.clear() + } +} diff --git a/libraries/compound/screenshots/Compound Icons - Dark.png b/libraries/compound/screenshots/Compound Icons - Dark.png index 2e3fd0d04d..6d5057eaac 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:7901fea2f578c8ed796160c9c08f417c61f4fb21580f958844fdf0cb794adf8a -size 239731 +oid sha256:78386a58e91be3213485f60909ec8d83cd535aabcec775aadf7b36e77d8542f0 +size 245853 diff --git a/libraries/compound/screenshots/Compound Icons - Light.png b/libraries/compound/screenshots/Compound Icons - Light.png index 6e1bfc80cd..f4f66317c3 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:245f012d419817f6557d92a71729b3b70092f24f0eba37f2f1fc431ad27592be -size 252969 +oid sha256:477ab18ffa1ca81ca5e4c9f30f4466b19e14fcaddf641fc860c6be19a0e33d4f +size 259185 diff --git a/libraries/compound/screenshots/Compound Icons - Rtl.png b/libraries/compound/screenshots/Compound Icons - Rtl.png index aa1b5d93a7..d0374a65fb 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:2d15c52b21cc279d306fa187cd0c318820109b5ec66270e6447e1b02e800eeba -size 254206 +oid sha256:7edeebf9876aecd262fceb7d55a4bfd30a859a69b708af8dd4538ffc0f9be419 +size 260400 diff --git a/libraries/compound/screenshots/Compound Vector Icons - Dark.png b/libraries/compound/screenshots/Compound Vector Icons - Dark.png index a01fbf18b4..0d8e07d9fa 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:85ef188fa3a27e42f4beafc899c1f3e7e8bcfad980ed76af6a03f76d70d6a511 -size 93807 +oid sha256:cc6cc2d2e89c7460217c2075046e41bef727407ee422dcdea23f161f48441904 +size 97346 diff --git a/libraries/compound/screenshots/Compound Vector Icons - Light.png b/libraries/compound/screenshots/Compound Vector Icons - Light.png index e227f1579c..74b4c859c7 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:f6e38386e95dc0c50384f06fca122ce14851ceff8ffc7865e394c1b4fccc5db6 -size 100555 +oid sha256:4df16aee4a3a8b10c33e9724904c72b8dc502725204d2f86cf62cc381b35f4a1 +size 103870 diff --git a/libraries/compound/src/main/assets/theme.iife.js b/libraries/compound/src/main/assets/theme.iife.js index e7205e4f81..41e18ba6a0 100644 --- a/libraries/compound/src/main/assets/theme.iife.js +++ b/libraries/compound/src/main/assets/theme.iife.js @@ -1 +1 @@ -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 +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),e=null),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(){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{let e=Math.atan2(this.luv_v,this.luv_u);this.lch_h=e*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(){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);let e=this.calcMaxChromaHsluv(this.hsluv_h);this.lch_c=e/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(){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);let e=this.calcMaxChromaHpluv();this.lch_c=e/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){let t=this._colors.filter(t=>t.name!==e.name);this._colors=t,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 5851a758f0..d8f81dbbbb 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 @@ -130,6 +130,9 @@ object CompoundIcons { @Composable fun Collapse(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_collapse) } + @Composable fun CollapseAll(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_collapse_all) + } @Composable fun Company(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_company) } @@ -142,6 +145,9 @@ object CompoundIcons { @Composable fun Copy(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_copy) } + @Composable fun Crop(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_crop) + } @Composable fun DarkMode(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_dark_mode) } @@ -202,6 +208,9 @@ object CompoundIcons { @Composable fun Expand(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_expand) } + @Composable fun ExpandAll(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_expand_all) + } @Composable fun Explore(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_explore) } @@ -229,6 +238,15 @@ object CompoundIcons { @Composable fun Filter(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_filter) } + @Composable fun FlipHorizontal(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_flip_horizontal) + } + @Composable fun FlipVertical(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_flip_vertical) + } + @Composable fun Folder(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_folder) + } @Composable fun Forward(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_forward) } @@ -496,6 +514,12 @@ object CompoundIcons { @Composable fun RotateRight(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_rotate_right) } + @Composable fun Save(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_save) + } + @Composable fun SaveSolid(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_save_solid) + } @Composable fun Search(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_search) } @@ -607,6 +631,9 @@ object CompoundIcons { @Composable fun Unpin(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_unpin) } + @Composable fun Unsave(): ImageVector { + return ImageVector.vectorResource(R.drawable.ic_compound_unsave) + } @Composable fun User(): ImageVector { return ImageVector.vectorResource(R.drawable.ic_compound_user) } @@ -735,10 +762,12 @@ object CompoundIcons { CloudSolid(), Code(), Collapse(), + CollapseAll(), Company(), Compose(), Computer(), Copy(), + Crop(), DarkMode(), Delete(), DevicePasskey(), @@ -759,6 +788,7 @@ object CompoundIcons { ErrorSolid(), ExitFullScreen(), Expand(), + ExpandAll(), Explore(), ExportArchive(), Extensions(), @@ -768,6 +798,9 @@ object CompoundIcons { FileError(), Files(), Filter(), + FlipHorizontal(), + FlipVertical(), + Folder(), Forward(), FullScreen(), Grid(), @@ -857,6 +890,8 @@ object CompoundIcons { Room(), RotateLeft(), RotateRight(), + Save(), + SaveSolid(), Search(), Section(), Send(), @@ -894,6 +929,7 @@ object CompoundIcons { Unknown(), UnknownSolid(), Unpin(), + Unsave(), User(), UserAdd(), UserAddSolid(), @@ -963,10 +999,12 @@ object CompoundIcons { R.drawable.ic_compound_cloud_solid, R.drawable.ic_compound_code, R.drawable.ic_compound_collapse, + R.drawable.ic_compound_collapse_all, R.drawable.ic_compound_company, R.drawable.ic_compound_compose, R.drawable.ic_compound_computer, R.drawable.ic_compound_copy, + R.drawable.ic_compound_crop, R.drawable.ic_compound_dark_mode, R.drawable.ic_compound_delete, R.drawable.ic_compound_device_passkey, @@ -987,6 +1025,7 @@ object CompoundIcons { R.drawable.ic_compound_error_solid, R.drawable.ic_compound_exit_full_screen, R.drawable.ic_compound_expand, + R.drawable.ic_compound_expand_all, R.drawable.ic_compound_explore, R.drawable.ic_compound_export_archive, R.drawable.ic_compound_extensions, @@ -996,6 +1035,9 @@ object CompoundIcons { R.drawable.ic_compound_file_error, R.drawable.ic_compound_files, R.drawable.ic_compound_filter, + R.drawable.ic_compound_flip_horizontal, + R.drawable.ic_compound_flip_vertical, + R.drawable.ic_compound_folder, R.drawable.ic_compound_forward, R.drawable.ic_compound_full_screen, R.drawable.ic_compound_grid, @@ -1085,6 +1127,8 @@ object CompoundIcons { R.drawable.ic_compound_room, R.drawable.ic_compound_rotate_left, R.drawable.ic_compound_rotate_right, + R.drawable.ic_compound_save, + R.drawable.ic_compound_save_solid, R.drawable.ic_compound_search, R.drawable.ic_compound_section, R.drawable.ic_compound_send, @@ -1122,6 +1166,7 @@ object CompoundIcons { R.drawable.ic_compound_unknown, R.drawable.ic_compound_unknown_solid, R.drawable.ic_compound_unpin, + R.drawable.ic_compound_unsave, R.drawable.ic_compound_user, R.drawable.ic_compound_user_add, R.drawable.ic_compound_user_add_solid, 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 f5a54bb7be..6bc6c89a63 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 @@ -175,6 +175,8 @@ data class SemanticColors( val iconTertiaryAlpha: Color, /** Used to separate core sections of the UI as well as containers */ val separatorPrimary: Color, + /** Secondary shade for separating sections of components or list items */ + val separatorSecondary: 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 35a1234854..e9334de0e7 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 @@ -103,6 +103,7 @@ val compoundColorsDark = SemanticColors( iconTertiary = DarkColorTokens.colorGray800, iconTertiaryAlpha = DarkColorTokens.colorAlphaGray800, separatorPrimary = DarkColorTokens.colorGray400, + separatorSecondary = DarkColorTokens.colorGray300, 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 802e3d56b0..a3ef51cff1 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 @@ -103,6 +103,7 @@ val compoundColorsHcDark = SemanticColors( iconTertiary = DarkHcColorTokens.colorGray800, iconTertiaryAlpha = DarkHcColorTokens.colorAlphaGray800, separatorPrimary = DarkHcColorTokens.colorGray400, + separatorSecondary = DarkHcColorTokens.colorGray300, 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 166f9ddafc..75153cb66b 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 @@ -103,6 +103,7 @@ val compoundColorsLight = SemanticColors( iconTertiary = LightColorTokens.colorGray800, iconTertiaryAlpha = LightColorTokens.colorAlphaGray800, separatorPrimary = LightColorTokens.colorGray400, + separatorSecondary = LightColorTokens.colorGray300, 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 8914b41bfc..fa40a2e2ab 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 @@ -103,6 +103,7 @@ val compoundColorsHcLight = SemanticColors( iconTertiary = LightHcColorTokens.colorGray800, iconTertiaryAlpha = LightHcColorTokens.colorAlphaGray800, separatorPrimary = LightHcColorTokens.colorGray400, + separatorSecondary = LightHcColorTokens.colorGray300, textActionAccent = LightHcColorTokens.colorGreen900, textActionPrimary = LightHcColorTokens.colorGray1400, textBadgeAccent = LightHcColorTokens.colorGreen1100, diff --git a/libraries/compound/src/main/res/drawable/ic_compound_collapse_all.xml b/libraries/compound/src/main/res/drawable/ic_compound_collapse_all.xml new file mode 100644 index 0000000000..7257b7d2c3 --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_collapse_all.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_crop.xml b/libraries/compound/src/main/res/drawable/ic_compound_crop.xml new file mode 100644 index 0000000000..c82f5cb686 --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_crop.xml @@ -0,0 +1,13 @@ + + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_expand_all.xml b/libraries/compound/src/main/res/drawable/ic_compound_expand_all.xml new file mode 100644 index 0000000000..fcef02880b --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_expand_all.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_flip_horizontal.xml b/libraries/compound/src/main/res/drawable/ic_compound_flip_horizontal.xml new file mode 100644 index 0000000000..90a3a98f44 --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_flip_horizontal.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_flip_vertical.xml b/libraries/compound/src/main/res/drawable/ic_compound_flip_vertical.xml new file mode 100644 index 0000000000..430e5eb3fa --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_flip_vertical.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_save.xml b/libraries/compound/src/main/res/drawable/ic_compound_save.xml new file mode 100644 index 0000000000..3bedcc272d --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_save.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_save_solid.xml b/libraries/compound/src/main/res/drawable/ic_compound_save_solid.xml new file mode 100644 index 0000000000..5d453834c7 --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_save_solid.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/compound/src/main/res/drawable/ic_compound_unsave.xml b/libraries/compound/src/main/res/drawable/ic_compound_unsave.xml new file mode 100644 index 0000000000..0dfddcfe46 --- /dev/null +++ b/libraries/compound/src/main/res/drawable/ic_compound_unsave.xml @@ -0,0 +1,13 @@ + + + + diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt index 01c3e787cf..895247f12f 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/AvatarColorsTest.kt @@ -8,19 +8,17 @@ package io.element.android.compound.screenshot -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.screenshot.utils.screenshotFile import io.element.android.compound.theme.AvatarColorsPreviewDark import io.element.android.compound.theme.AvatarColorsPreviewLight +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class AvatarColorsTest { +class AvatarColorsTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "xxhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt index 3e8ce06cdb..97926fc291 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundIconTest.kt @@ -11,7 +11,6 @@ package io.element.android.compound.screenshot import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.previews.IconsCompoundPreviewDark import io.element.android.compound.previews.IconsCompoundPreviewLight @@ -21,15 +20,14 @@ import io.element.android.compound.screenshot.utils.screenshotFile import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.Theme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.collections.immutable.toImmutableList import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class CompoundIconTest { +class CompoundIconTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "w1024dp-h2048dp") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt index c4822f680e..ec27acd836 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/CompoundTypographyTest.kt @@ -8,18 +8,16 @@ package io.element.android.compound.screenshot -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.previews.CompoundTypographyPreview import io.element.android.compound.screenshot.utils.screenshotFile +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class CompoundTypographyTest { +class CompoundTypographyTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "h2048dp-xxhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt index 74e6f3010d..5f858b8622 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/ForcedDarkElementThemeTest.kt @@ -18,20 +18,18 @@ import androidx.compose.material3.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.compound.screenshot.utils.screenshotFile import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ForcedDarkElementTheme +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class ForcedDarkElementThemeTest { +class ForcedDarkElementThemeTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "xxhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt index deb27c6519..c0c3d11555 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/LegacyColorsTest.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.previews.ColorPreview import io.element.android.compound.screenshot.utils.screenshotFile @@ -26,14 +25,13 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.LinkColor import io.element.android.compound.theme.SnackBarLabelColorDark import io.element.android.compound.theme.SnackBarLabelColorLight +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class LegacyColorsTest { +class LegacyColorsTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "xxhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt index 282ca0e69a..2e1368ff54 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialColorSchemeTest.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.screenshot.utils.screenshotFile import io.element.android.compound.theme.ColorsSchemeDarkHcPreview @@ -26,14 +25,13 @@ import io.element.android.compound.theme.ColorsSchemeDarkPreview import io.element.android.compound.theme.ColorsSchemeLightHcPreview import io.element.android.compound.theme.ColorsSchemeLightPreview import io.element.android.compound.theme.ElementTheme +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class MaterialColorSchemeTest { +class MaterialColorSchemeTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "h2048dp-xhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt index 2aaeed43a5..4d21e77aed 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTextTest.kt @@ -8,18 +8,16 @@ package io.element.android.compound.screenshot -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.screenshot.utils.screenshotFile import io.element.android.compound.theme.MaterialTextPreview +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class MaterialTextTest { +class MaterialTextTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "w480dp-h1200dp-xxhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt index 3d8256e2af..3c07ff48f5 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialTypographyTest.kt @@ -8,18 +8,16 @@ package io.element.android.compound.screenshot -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.previews.TypographyPreview import io.element.android.compound.screenshot.utils.screenshotFile +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class MaterialTypographyTest { +class MaterialTypographyTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "h2048dp-xxhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt index dcbbc9d4ac..2f1abc55f5 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/MaterialYouThemeTest.kt @@ -19,20 +19,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.previews.ColorsSchemePreview import io.element.android.compound.screenshot.utils.screenshotFile import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.Theme +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class MaterialYouThemeTest { +class MaterialYouThemeTest : RobolectricTest() { @Test @Config(sdk = [35], qualifiers = "h2048dp-xhdpi") fun screenshots() { diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt index f36d0d2ee5..95e34fb3d9 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/screenshot/SemanticColorsTest.kt @@ -8,21 +8,19 @@ package io.element.android.compound.screenshot -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.captureRoboImage import io.element.android.compound.previews.CompoundSemanticColorsDark import io.element.android.compound.previews.CompoundSemanticColorsDarkHc import io.element.android.compound.previews.CompoundSemanticColorsLight import io.element.android.compound.previews.CompoundSemanticColorsLightHc import io.element.android.compound.screenshot.utils.screenshotFile +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode -@RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -class SemanticColorsTest { +class SemanticColorsTest : RobolectricTest() { @Config(sdk = [35], qualifiers = "h2000dp-xhdpi") @Test fun screenshots() { diff --git a/libraries/cryptography/api/build.gradle.kts b/libraries/cryptography/api/build.gradle.kts index 9ce26419d8..74fc5f6ecc 100644 --- a/libraries/cryptography/api/build.gradle.kts +++ b/libraries/cryptography/api/build.gradle.kts @@ -13,3 +13,7 @@ plugins { android { namespace = "io.element.android.libraries.cryptography.api" } + +dependencies { + implementation(libs.coroutines.core) +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt index ba6c10dbe0..b210664d9f 100644 --- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.cryptography.api +import kotlinx.coroutines.flow.Flow import javax.crypto.SecretKey /** @@ -15,16 +16,18 @@ import javax.crypto.SecretKey * Implementation should be able to store the generated key securely. */ interface SecretKeyRepository { + fun hasKey(alias: String): Flow + /** * Get or create a secret key for a given alias. * @param alias the alias to use * @param requiresUserAuthentication true if the key should be protected by user authentication */ - fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey + suspend fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey /** * Delete the secret key for a given alias. * @param alias the alias to use */ - fun deleteKey(alias: String) + suspend fun deleteKey(alias: String) } diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts index 454432de6f..3a1f55126e 100644 --- a/libraries/cryptography/impl/build.gradle.kts +++ b/libraries/cryptography/impl/build.gradle.kts @@ -21,6 +21,7 @@ setupDependencyInjection() dependencies { implementation(projects.libraries.di) + implementation(libs.coroutines.core) api(projects.libraries.cryptography.api) testCommonDependencies(libs) diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt index 46572ef047..bcd38695c4 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt @@ -13,11 +13,16 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.SingleIn import io.element.android.libraries.cryptography.api.AESEncryptionSpecs import io.element.android.libraries.cryptography.api.SecretKeyRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber import java.security.KeyStore import java.security.KeyStoreException +import java.util.concurrent.ConcurrentHashMap import javax.crypto.KeyGenerator import javax.crypto.SecretKey @@ -25,13 +30,23 @@ import javax.crypto.SecretKey * Default implementation of [SecretKeyRepository] that uses the Android Keystore to store the keys. * The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. */ +@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class KeyStoreSecretKeyRepository( private val keyStore: KeyStore, ) : SecretKeyRepository { + private val hasKeyMap = ConcurrentHashMap>() + + @Suppress("RunCatchingNotAllowed") + override fun hasKey(alias: String): Flow { + return hasKeyMap.getOrPut(alias) { + MutableStateFlow(runCatching { keyStore.containsAlias(alias) }.getOrDefault(false)) + }.asStateFlow() + } + // False positive lint issue @SuppressLint("WrongConstant") - override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { + override suspend fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey return if (secretKeyEntry == null) { @@ -46,15 +61,22 @@ class KeyStoreSecretKeyRepository( .setUserAuthenticationRequired(requiresUserAuthentication) .build() generator.init(keyGenSpec) - generator.generateKey() + generator.generateKey().also { + hasKeyMap.getOrPut(alias) { + MutableStateFlow(true) + }.emit(true) + } } else { secretKeyEntry } } - override fun deleteKey(alias: String) { + override suspend fun deleteKey(alias: String) { try { keyStore.deleteEntry(alias) + hasKeyMap.getOrPut(alias) { + MutableStateFlow(false) + }.emit(false) } catch (e: KeyStoreException) { Timber.e(e) } diff --git a/libraries/cryptography/test/build.gradle.kts b/libraries/cryptography/test/build.gradle.kts index eaa621d53a..5cf04a9754 100644 --- a/libraries/cryptography/test/build.gradle.kts +++ b/libraries/cryptography/test/build.gradle.kts @@ -16,4 +16,5 @@ android { dependencies { api(projects.libraries.cryptography.api) + implementation(libs.coroutines.core) } diff --git a/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt index 0e301553ea..507325f45b 100644 --- a/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt +++ b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyRepository.kt @@ -10,20 +10,39 @@ package io.element.android.libraries.cryptography.test import io.element.android.libraries.cryptography.api.AESEncryptionSpecs import io.element.android.libraries.cryptography.api.SecretKeyRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.concurrent.ConcurrentHashMap import javax.crypto.KeyGenerator import javax.crypto.SecretKey class SimpleSecretKeyRepository : SecretKeyRepository { private var secretKeyForAlias = HashMap() - override fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { + private val hasKeyMap = ConcurrentHashMap>() + + override fun hasKey(alias: String): Flow { + return hasKeyMap.getOrPut(alias) { + MutableStateFlow(false) + }.asStateFlow() + } + + override suspend fun getOrCreateKey(alias: String, requiresUserAuthentication: Boolean): SecretKey { return secretKeyForAlias.getOrPut(alias) { - generateKey() + generateKey().also { + hasKeyMap.getOrPut(alias) { + MutableStateFlow(true) + }.emit(true) + } } } - override fun deleteKey(alias: String) { + override suspend fun deleteKey(alias: String) { secretKeyForAlias.remove(alias) + hasKeyMap.getOrPut(alias) { + MutableStateFlow(false) + }.emit(false) } private fun generateKey(): SecretKey { diff --git a/libraries/dateformatter/impl/src/main/res/values-ca/translations.xml b/libraries/dateformatter/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..491793c728 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s a les %2$s" + "Aquest mes" + diff --git a/libraries/dateformatter/impl/src/main/res/values-zh/translations.xml b/libraries/dateformatter/impl/src/main/res/values-zh/translations.xml index 9fab311a1d..d1ee550b73 100644 --- a/libraries/dateformatter/impl/src/main/res/values-zh/translations.xml +++ b/libraries/dateformatter/impl/src/main/res/values-zh/translations.xml @@ -1,5 +1,5 @@ - "%1$s在 %2$s" + "于 %1$s %2$s" "本月" diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt index b4800729d5..dfea6adc46 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt @@ -9,17 +9,15 @@ package io.element.android.libraries.dateformatter.impl import android.os.Build -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.dateformatter.api.DateFormatterMode +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import kotlin.time.Instant -@RunWith(AndroidJUnit4::class) @Config(qualifiers = "fr", sdk = [Build.VERSION_CODES.TIRAMISU]) -class DefaultDateFormatterFrTest { +class DefaultDateFormatterFrTest : RobolectricTest() { @Test fun `test null`() { val now = "1980-04-06T18:35:24.00Z" diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt index 94b6fe983e..f761d833ab 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt @@ -9,17 +9,15 @@ package io.element.android.libraries.dateformatter.impl import android.os.Build -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.dateformatter.api.DateFormatterMode +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import kotlin.time.Instant -@RunWith(AndroidJUnit4::class) @Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU]) -class DefaultDateFormatterTest { +class DefaultDateFormatterTest : RobolectricTest() { @Test fun `test null`() { val now = "1980-04-06T18:35:24.00Z" diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDurationFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDurationFormatterTest.kt index 3fee5dbb0d..6b6fbbb2c1 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDurationFormatterTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDurationFormatterTest.kt @@ -8,10 +8,9 @@ package io.element.android.libraries.dateformatter.impl import android.os.Build -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config import java.util.Locale import kotlin.time.Duration.Companion.hours @@ -19,9 +18,8 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -@RunWith(AndroidJUnit4::class) @Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU]) -class DefaultDurationFormatterTest { +class DefaultDurationFormatterTest : RobolectricTest() { private fun createDurationFormatter(): DefaultDurationFormatter { return DefaultDurationFormatter( localeChangeObserver = {}, diff --git a/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt index 7f563ddd7f..a6502aabf1 100644 --- a/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt +++ b/libraries/deeplink/impl/src/test/kotlin/io/element/android/libraries/deeplink/impl/DefaultDeeplinkParserTest.kt @@ -21,12 +21,10 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.tests.testutils.assertThrowsInDebug +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultDeeplinkParserTest { +class DefaultDeeplinkParserTest : RobolectricTest() { companion object { const val A_URI = "elementx://open/@alice:server.org" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt index a051fd4c80..3bbd3c12a7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/CounterAtom.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.dp @@ -37,13 +38,18 @@ private const val MAX_COUNT_STRING = "+$MAX_COUNT" * @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING]. * If the number is less than 1, the counter will not be displayed. * @param modifier The modifier to apply to this layout. + * @param containerColor The background color of the counter. When null, uses [isCritical] to pick a default. + * @param contentColor The text color inside the counter. When null, uses [textOnSolidPrimary]. * @param textStyle The style to apply to the text inside the counter. * @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme. + * Only used when [containerColor] is null. */ @Composable fun CounterAtom( count: Int, modifier: Modifier = Modifier, + containerColor: Color? = null, + contentColor: Color? = null, textStyle: TextStyle = CounterAtomDefaults.textStyle, isCritical: Boolean = false, ) { @@ -65,7 +71,7 @@ fun CounterAtom( .size(squareSize.toDp() + 1.dp) .clip(CircleShape) .background( - if (isCritical) { + containerColor ?: if (isCritical) { ElementTheme.colors.iconCriticalPrimary } else { ElementTheme.colors.iconAccentPrimary @@ -76,7 +82,7 @@ fun CounterAtom( modifier = Modifier.align(Alignment.Center), text = countAsText, style = textStyle, - color = ElementTheme.colors.textOnSolidPrimary, + color = contentColor ?: ElementTheme.colors.textOnSolidPrimary, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt index d2db3aec8e..ff49bab06f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/UnreadIndicatorAtom.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.designsystem.atomic.atoms import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable @@ -29,19 +30,32 @@ import io.element.android.libraries.designsystem.theme.unreadIndicator fun UnreadIndicatorAtom( modifier: Modifier = Modifier, size: Dp = 12.dp, + count: Long? = null, color: Color = ElementTheme.colors.unreadIndicator, isVisible: Boolean = true, contentDescription: String? = null, ) { - Box( - modifier = modifier - .semantics { + when { + !isVisible -> Spacer(modifier = modifier.size(size)) + count != null && count >= 1 -> CounterAtom( + count = count.toInt(), + modifier = modifier.semantics { contentDescription?.let { this.contentDescription = it } - } - .size(size) - .clip(CircleShape) - .background(if (isVisible) color else Color.Transparent) - ) + }, + containerColor = color, + contentColor = ElementTheme.colors.bgCanvasDefault, + textStyle = ElementTheme.typography.fontBodySmMedium, + ) + else -> Box( + modifier = modifier + .semantics { + contentDescription?.let { this.contentDescription = it } + } + .size(size) + .clip(CircleShape) + .background(color), + ) + } } @PreviewsDayNight diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt index b62f634ece..a29e1b743c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/FlowStepPage.kt @@ -71,7 +71,7 @@ fun FlowStepPage( }, header = { IconTitleSubtitleMolecule( - modifier = Modifier.padding(bottom = 16.dp), + modifier = Modifier.padding(bottom = 16.dp, start = 8.dp, end = 8.dp), title = title, subTitle = subTitle, iconStyle = iconStyle, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Announcement.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Announcement.kt index dcd3f8fa21..037c37e3a8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Announcement.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Announcement.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -148,7 +150,11 @@ private fun TitleAndDescription( text = title, style = ElementTheme.typography.fontBodyLgMedium, color = titleColor, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .semantics { + heading() + }, ) if (trailingContent != null) { Spacer(Modifier.width(12.dp)) 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 9e783d605c..4940f2f965 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 @@ -54,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.preview.USER_NAME_ALICE import io.element.android.libraries.designsystem.utils.CommonDrawables private val PIN_WIDTH = 42.dp @@ -395,7 +396,7 @@ private object LocationPinRenderer { internal fun LocationPinPreview() = ElementPreview { val sampleAvatarData = AvatarData( id = "@alice:matrix.org", - name = "Alice", + name = USER_NAME_ALICE, url = null, size = AvatarSize.SelectedUser ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt index 870ffe44a9..53ef5593ed 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt @@ -8,10 +8,12 @@ package io.element.android.libraries.designsystem.components.avatar +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE + fun anAvatarData( - // Let's the id not start with a 'a'. + // Let the id not start with a 'a'. id: String = "@id_of_alice:server.org", - name: String? = "Alice", + name: String? = USER_NAME_ALICE, url: String? = null, size: AvatarSize = AvatarSize.RoomListItem, ) = AvatarData( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/DmAvatars.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/DmAvatars.kt deleted file mode 100644 index c5b870507b..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/DmAvatars.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.designsystem.components.avatar - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection -import io.element.android.libraries.designsystem.preview.ElementThemedPreview -import io.element.android.libraries.designsystem.preview.PreviewGroup -import io.element.android.libraries.designsystem.text.toPx -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.ui.strings.CommonStrings - -/** Ratio between the box size (120 on Figma) and the avatar size (75 on Figma). */ -private const val SIZE_RATIO = 1.6f - -/** - * https://www.figma.com/design/A2pAEvTEpJZBiOPUlcMnKi/Settings-%2B-Room-Details-(new)?node-id=1787-56333 - */ -@Composable -fun DmAvatars( - userAvatarData: AvatarData, - otherUserAvatarData: AvatarData, - openAvatarPreview: (url: String) -> Unit, - openOtherAvatarPreview: (url: String) -> Unit, - modifier: Modifier = Modifier, -) { - val boxSize = userAvatarData.size.dp * SIZE_RATIO - val boxSizePx = boxSize.toPx() - val otherAvatarRadius = otherUserAvatarData.size.dp.toPx() / 2 - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - Box( - modifier = modifier.size(boxSize), - ) { - // Draw user avatar and cut top end corner - Avatar( - avatarData = userAvatarData, - avatarType = AvatarType.User, - contentDescription = stringResource(CommonStrings.a11y_your_avatar), - modifier = Modifier - .align(Alignment.BottomStart) - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen - } - .drawWithContent { - drawContent() - val xOffset = if (isRtl) { - size.width - boxSizePx + otherAvatarRadius - } else { - boxSizePx - otherAvatarRadius - } - drawCircle( - color = Color.Black, - center = Offset( - x = xOffset, - y = size.height - (boxSizePx - otherAvatarRadius), - ), - radius = otherAvatarRadius / 0.9f, - blendMode = BlendMode.Clear, - ) - } - .clip(CircleShape) - .clickable( - enabled = userAvatarData.url != null, - onClickLabel = stringResource(CommonStrings.action_view), - ) { - userAvatarData.url?.let { openAvatarPreview(it) } - } - ) - // Draw other user avatar - Avatar( - avatarData = otherUserAvatarData, - avatarType = AvatarType.User, - contentDescription = stringResource(CommonStrings.a11y_other_user_avatar), - modifier = Modifier - .align(Alignment.TopEnd) - .clip(CircleShape) - .clickable( - enabled = otherUserAvatarData.url != null, - onClickLabel = stringResource(CommonStrings.action_view), - ) { - otherUserAvatarData.url?.let { openOtherAvatarPreview(it) } - } - .testTag(TestTags.memberDetailAvatar) - ) - } -} - -@Preview(group = PreviewGroup.Avatars) -@Composable -internal fun DmAvatarsPreview() = ElementThemedPreview { - val size = AvatarSize.DmCluster - DmAvatars( - userAvatarData = anAvatarData( - id = "Alice", - name = "Alice", - size = size, - ), - otherUserAvatarData = anAvatarData( - id = "Bob", - name = "Bob", - size = size, - ), - openAvatarPreview = {}, - openOtherAvatarPreview = {}, - ) -} - -@Preview(group = PreviewGroup.Avatars) -@Composable -internal fun DmAvatarsRtlPreview() { - CompositionLocalProvider( - LocalLayoutDirection provides LayoutDirection.Rtl, - ) { - DmAvatarsPreview() - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/BringIntoViewOnImeVisible.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/BringIntoViewOnImeVisible.kt new file mode 100644 index 0000000000..155172c96f --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/BringIntoViewOnImeVisible.kt @@ -0,0 +1,49 @@ +/* + * 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.libraries.designsystem.modifiers + +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +/** + * Keeps the focused field visible above the keyboard. Intended for a field inside a scrollable + * container: the field is never clipped by a pinned footer, but the IME is shown only after focus + * arrives, so we re-request bringIntoView once it is visible to scroll the field back into the + * reduced viewport. + */ +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun Modifier.bringIntoViewOnImeVisible(): Modifier { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + var isFocused by remember { mutableStateOf(false) } + val isImeVisible = WindowInsets.isImeVisible + LaunchedEffect(isImeVisible, isFocused) { + if (isImeVisible && isFocused) { + // Delay to ensure the keyboard is fully shown before scrolling the field into view. + delay(100.milliseconds) + bringIntoViewRequester.bringIntoView() + } + } + return this + .bringIntoViewRequester(bringIntoViewRequester) + .onFocusChanged { isFocused = it.isFocused } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewData.kt new file mode 100644 index 0000000000..66a6043634 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewData.kt @@ -0,0 +1,27 @@ +/* + * 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.preview + +const val USER_NAME_ALICE = "Alice" +const val USER_NAME_BOB = "Bob" +const val USER_NAME_CHARLIE = "Charlie" +const val USER_NAME_CAROL = "Carol" +const val USER_NAME_DAVID = "David" +const val USER_NAME_EVE = "Eve" +const val USER_NAME_JOHN_DOE = "John Doe" +const val USER_NAME_JUSTIN = "Justin" +const val USER_NAME_MALLORY = "Mallory" +const val USER_NAME_SENDER = "Sender" +const val USER_NAME_SUSIE = "Susie" +const val USER_NAME_VICTOR = "Victor" +const val USER_NAME_WALTER = "Walter" + +const val ROOM_NAME = "Room name" +const val SPACE_NAME = "Space name" + +const val LAST_MESSAGE = "Last message" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewWithLargeHeight.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewWithLargeHeight.kt index 163ee21590..8d745ec0e7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewWithLargeHeight.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/preview/PreviewWithLargeHeight.kt @@ -16,3 +16,6 @@ import androidx.compose.ui.tooling.preview.Preview */ @Preview(heightDp = 1000) annotation class PreviewWithLargeHeight + +@Preview(heightDp = 1600) +annotation class PreviewWithExtraLargeHeight diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalDivider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalDivider.kt index b4c7eddf31..0a3d717b09 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalDivider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/HorizontalDivider.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.material3.DividerDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -18,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -25,7 +25,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup fun HorizontalDivider( modifier: Modifier = Modifier, thickness: Dp = ElementDividerDefaults.thickness, - color: Color = DividerDefaults.color, + color: Color = ElementDividerDefaults.color, ) { androidx.compose.material3.HorizontalDivider( modifier = modifier, @@ -35,7 +35,9 @@ fun HorizontalDivider( } object ElementDividerDefaults { - val thickness = 0.5.dp + val thickness = 1.dp + val color + @Composable get() = ElementTheme.colors.separatorSecondary } @Preview(group = PreviewGroup.Dividers) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt index 403ed6da97..3a86d72e7d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSectionHeader.kt @@ -17,6 +17,8 @@ import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -48,6 +50,9 @@ fun ListSectionHeader( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( + modifier = Modifier.semantics { + heading() + }, text = title, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt index 689cba727f..ec5f7ce456 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -24,6 +24,8 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.contentColorFor import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.movableContentOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -88,14 +90,15 @@ fun ModalBottomSheet( dragHandle = dragHandle, contentWindowInsets = contentWindowInsets, ) { + val movableContent = remember { movableContentOf { content() } } if (scrollable) { Column( modifier = Modifier.verticalScroll(rememberScrollState()), ) { - content() + movableContent() } } else { - content() + movableContent() } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/PasswordVisibilityToggle.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/PasswordVisibilityToggle.kt new file mode 100644 index 0000000000..9154ec6292 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/PasswordVisibilityToggle.kt @@ -0,0 +1,37 @@ +/* + * 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.theme.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.ui.strings.CommonStrings + +/** + * Show/hide toggle for a password [TextField], intended for its `trailingIcon` slot. + * Shared so every password field reveals plaintext the same way and announces the + * same accessibility labels. + */ +@Composable +fun PasswordVisibilityToggle( + visible: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier.clickable(onClick = onToggle)) { + Icon( + imageVector = if (visible) CompoundIcons.VisibilityOn() else CompoundIcons.VisibilityOff(), + contentDescription = stringResource( + if (visible) CommonStrings.a11y_hide_password else CommonStrings.a11y_show_password + ), + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt index 56bb070228..566c92d5cf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcher.kt @@ -18,39 +18,35 @@ import androidx.compose.runtime.remember import androidx.compose.ui.res.stringResource import io.element.android.libraries.designsystem.theme.components.Snackbar import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow /** * A global dispatcher of [SnackbarMessage] to be displayed in [Snackbar] via a [SnackbarHostState]. + * + * The current head of the queue is exposed as a [MutableStateFlow] so that every collector — including + * hosts that subscribe *after* a message was posted (e.g. a screen recomposing as a flow pops back to + * it) — observes the current message. An earlier mutex-gated implementation delivered each message to + * whichever single collector happened to be parked on the lock, which could starve the host that was + * actually on screen, dropping the snackbar. */ class SnackbarDispatcher { - private val queueMutex = Mutex() private val snackBarMessageQueue = ArrayDeque() - val snackbarMessage: Flow = flow { - while (currentCoroutineContext().isActive) { - queueMutex.lock() - emit(snackBarMessageQueue.firstOrNull()) - } - } + private val currentMessage = MutableStateFlow(null) + val snackbarMessage: Flow = currentMessage.asStateFlow() + + @Synchronized fun post(message: SnackbarMessage) { - if (snackBarMessageQueue.isEmpty()) { - snackBarMessageQueue.add(message) - if (queueMutex.isLocked) queueMutex.unlock() - } else { - snackBarMessageQueue.add(message) - } + snackBarMessageQueue.add(message) + currentMessage.value = snackBarMessageQueue.firstOrNull() } + @Synchronized fun clear() { - if (snackBarMessageQueue.isNotEmpty()) { - snackBarMessageQueue.removeFirstOrNull() - if (queueMutex.isLocked) queueMutex.unlock() - } + snackBarMessageQueue.removeFirstOrNull() + currentMessage.value = snackBarMessageQueue.firstOrNull() } } diff --git a/libraries/designsystem/src/main/res/drawable-night/sample_map.png b/libraries/designsystem/src/main/res/drawable-night/sample_map.png new file mode 100644 index 0000000000..7c58760c15 Binary files /dev/null and b/libraries/designsystem/src/main/res/drawable-night/sample_map.png differ diff --git a/libraries/designsystem/src/main/res/drawable/sample_map.png b/libraries/designsystem/src/main/res/drawable/sample_map.png new file mode 100644 index 0000000000..9bda2e15f2 Binary files /dev/null and b/libraries/designsystem/src/main/res/drawable/sample_map.png differ diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTest.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTest.kt index 91c32b1da0..fe17aa0b6c 100644 --- a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTest.kt +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/snackbar/SnackbarDispatcherTest.kt @@ -54,6 +54,27 @@ class SnackbarDispatcherTest { } } + @Test + fun `a host that subscribes after a message is posted still receives it`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + val message = SnackbarMessage(0) + // A first host is already collecting (mirrors a background screen still subscribed). + snackbarDispatcher.snackbarMessage.test { + assertThat(awaitItem()).isNull() + snackbarDispatcher.post(message) + assertThat(awaitItem()).isEqualTo(message) + + // A second host subscribes only afterwards — e.g. the screen a flow pops back to once the + // message has already been posted. It must observe the current message rather than being + // starved waiting for the next post (regression: an earlier mutex-gated implementation + // delivered each post to a single parked collector, dropping the snackbar for any host + // that subscribed later). + snackbarDispatcher.snackbarMessage.test { + assertThat(awaitItem()).isEqualTo(message) + } + } + } + @Test fun `given 2 message emissions, the next message is displayed only after a call to clear`() = runTest { val snackbarDispatcher = SnackbarDispatcher() diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt index e4ac1a949f..84ab2d2a18 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt @@ -9,30 +9,54 @@ package io.element.encrypteddb import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlSchema import app.cash.sqldelight.driver.android.AndroidSqliteDriver -import io.element.encrypteddb.passphrase.PassphraseProvider +import io.element.android.libraries.androidutils.crypto.ClientSecret +import io.element.encrypteddb.passphrase.DatabaseSecretProvider import net.zetetic.database.sqlcipher.SupportOpenHelperFactory /** * Creates an encrypted version of the [SqlDriver] using SQLCipher's [SupportOpenHelperFactory]. - * @param passphraseProvider Provides the passphrase needed to use the SQLite database with SQLCipher. + * @param databaseSecretProvider Provides the passphrase needed to use the SQLite database with SQLCipher. */ class SqlCipherDriverFactory( - private val passphraseProvider: PassphraseProvider, + private val databaseSecretProvider: DatabaseSecretProvider, ) { + companion object { + init { + System.loadLibrary("sqlcipher") + } + } + /** * Returns a valid [SqlDriver] with SQLCipher support. * @param schema The SQLite DB schema. * @param name The name of the database to create. * @param context Android [Context], used to instantiate the driver. + * @param onUpgradeCallback Optional callback to handle database upgrades, which will be called in the [AndroidSqliteDriver.Callback.onUpgrade] method. */ - fun create(schema: SqlSchema>, name: String, context: Context): SqlDriver { - System.loadLibrary("sqlcipher") - val passphrase = passphraseProvider.getPassphrase() - val factory = SupportOpenHelperFactory(passphrase) - return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory) + fun create( + schema: SqlSchema>, + name: String, + context: Context, + onUpgradeCallback: ((driver: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) -> Unit)? = null, + ): SqlDriver { + val key = when (val secret = databaseSecretProvider.getSecret()) { + // For a raw key, we need the blob representation (`x'...'`) as bytes + is ClientSecret.RawKey -> secret.formattedAsString().toByteArray() + // For a passphrase, we need the bytes for the hex string representation + is ClientSecret.Passphrase -> secret.formattedAsString().hexToByteArray() + } + val factory = SupportOpenHelperFactory(key) + return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory, callback = object : AndroidSqliteDriver.Callback( + schema + ) { + override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { + onUpgradeCallback?.invoke(db, oldVersion, newVersion) + } + }) } } diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/PassphraseProvider.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/DatabaseSecretProvider.kt similarity index 51% rename from libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/PassphraseProvider.kt rename to libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/DatabaseSecretProvider.kt index a72dc73596..140a5d8de2 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/PassphraseProvider.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/DatabaseSecretProvider.kt @@ -8,12 +8,19 @@ package io.element.encrypteddb.passphrase +import io.element.android.libraries.androidutils.crypto.ClientSecret + /** * An abstraction to implement secure providers for SQLCipher passphrases. */ -interface PassphraseProvider { +interface DatabaseSecretProvider { /** - * Returns a passphrase for SQLCipher in [ByteArray] format. + * Returns a secret for SQLCipher. */ - fun getPassphrase(): ByteArray + fun getSecret(): ClientSecret + + /** + * Resets the passphrase, for example by deleting the persisted secret. Returns `true` if the reset was successful, `false` otherwise. + */ + fun reset(): Boolean } diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomDatabaseSecretProvider.kt similarity index 56% rename from libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt rename to libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomDatabaseSecretProvider.kt index fe78a4a8da..246a65da07 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomDatabaseSecretProvider.kt @@ -9,35 +9,41 @@ package io.element.encrypteddb.passphrase import android.content.Context +import io.element.android.libraries.androidutils.crypto.ClientSecret import io.element.encrypteddb.crypto.EncryptedFile import java.io.File import java.security.SecureRandom /** - * Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile]. + * Provides a secure secret for SQLCipher by generating a random secret and storing it into an [EncryptedFile]. * @param context Android [Context], used by [EncryptedFile] for cryptographic operations. * @param file Destination file where the key will be stored. - * @param secretSize Length of the generated secret. + * @param secretSizeBytes Length of the generated secret. */ -class RandomSecretPassphraseProvider( +class RandomDatabaseSecretProvider( private val context: Context, private val file: File, - private val secretSize: Int = 256, -) : PassphraseProvider { - override fun getPassphrase(): ByteArray { + private val secretSizeBytes: Int = 32, +) : DatabaseSecretProvider { + override fun getSecret(): ClientSecret { val encryptedFile = EncryptedFile(context, file) - return if (!file.exists()) { + val bytes = if (!file.exists()) { val secret = generateSecret() encryptedFile.openFileOutput().use { it.write(secret) } secret } else { encryptedFile.openFileInput().use { it.readBytes() } } + return ClientSecret.fromRawBytes(bytes) + } + + override fun reset(): Boolean { + return file.delete() } private fun generateSecret(): ByteArray { - val buffer = ByteArray(size = secretSize) - SecureRandom().nextBytes(buffer) - return buffer + // Generate a random secret of the specified size using a secure random generator. + return ByteArray(size = secretSizeBytes) + .also { SecureRandom().nextBytes(it) } } } diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/utils/ReplaceDatabaseKey.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/utils/ReplaceDatabaseKey.kt new file mode 100644 index 0000000000..fafb18794b --- /dev/null +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/utils/ReplaceDatabaseKey.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.encrypteddb.utils + +import androidx.sqlite.db.SupportSQLiteDatabase +import io.element.encrypteddb.passphrase.RandomDatabaseSecretProvider +import timber.log.Timber + +/** + * A utility class to replace the encryption key of an existing SQLCipher database. + * This is used during database migrations when we want to change the encryption key. + * + * @param databaseSecretProvider The provider for generating new secrets. + */ +class ReplaceDatabaseKey( + private val databaseSecretProvider: RandomDatabaseSecretProvider +) { + fun replaceKey(name: String, database: SupportSQLiteDatabase) { + Timber.d("Re-keying database $name") + // Reset the passphrase provider to generate a new passphrase + databaseSecretProvider.reset() + + // Get the new secret and convert it to the format expected by SQLCipher + val newSecret = databaseSecretProvider.getSecret() + val key = newSecret.formattedAsString() + + // Use the PRAGMA rekey command to change the encryption key of the database + database.query("PRAGMA rekey = \"$key\";").close() + + // Verify that the database can be accessed with the new key by running a simple query + val result = database.query("select count(*) from sqlite_master").use { cursor -> + if (cursor.moveToNext()) cursor.getLong(0) else -1L + } + if (result >= 0) { + Timber.d("Re-keying database $name completed") + } else { + Timber.e("Re-keying database $name didn't work as expected.") + } + } +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt index 7878245aca..c8afba3603 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt @@ -77,26 +77,28 @@ class DefaultPinnedMessagesBannerFormatter( messageType.toPlainText(permalinkParser) } is VideoMessageType -> { - messageType.bestDescription.prefixWith(CommonStrings.common_video) + messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_video) } is ImageMessageType -> { - messageType.bestDescription.prefixWith(CommonStrings.common_image) + messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_image) } is StickerMessageType -> { - messageType.bestDescription.prefixWith(CommonStrings.common_sticker) + messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_sticker) } is LocationMessageType -> { messageType.body.prefixWith(CommonStrings.common_shared_location) } is FileMessageType -> { - messageType.bestDescription.prefixWith(CommonStrings.common_file) + messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_file) } is AudioMessageType -> { - messageType.bestDescription.prefixWith(CommonStrings.common_audio) + messageType.toPlainText(permalinkParser).prefixWith(CommonStrings.common_audio) } is VoiceMessageType -> { - // In this case, do not use bestDescription, because the filename is useless, only use the caption if available. - messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message)) + messageType + .toPlainText(permalinkParser, "") + .takeIf { it.isNotEmpty() } + ?.prefixWith(sp.getString(CommonStrings.common_voice_message)) ?: sp.getString(CommonStrings.common_voice_message) } is OtherMessageType -> { diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt index 3ecd7819e5..e8a462da16 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLatestEventFormatter.kt @@ -54,6 +54,7 @@ class DefaultRoomLatestEventFormatter( private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, private val stateContentFormatter: StateContentFormatter, + private val rtcNotificationContentFormatter: RtcNotificationContentFormatter, private val permalinkParser: PermalinkParser, ) : RoomLatestEventFormatter { override fun format( @@ -92,8 +93,8 @@ class DefaultRoomLatestEventFormatter( message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is StickerContent -> { - val message = sp.getString(CommonStrings.common_sticker) + " (" + content.bestDescription + ")" - message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) + content.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker)) + .prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is UnableToDecryptContent -> { val message = sp.getString(CommonStrings.common_waiting_for_decryption_key) @@ -109,8 +110,8 @@ class DefaultRoomLatestEventFormatter( stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.RoomList) } is PollContent -> { - val message = sp.getString(CommonStrings.common_poll_summary, content.question) - message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) + content.question.prefixWith(sp.getString(CommonStrings.common_poll_summary_prefix)) + .prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is FailedToParseMessageLikeContent, is FailedToParseStateContent, is UnknownContent -> { val message = sp.getString(CommonStrings.common_unsupported_event) @@ -121,7 +122,7 @@ class DefaultRoomLatestEventFormatter( message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) } is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call) - is CallNotifyContent -> sp.getString(CommonStrings.common_call_started) + is CallNotifyContent -> rtcNotificationContentFormatter.format(content, isDmRoom) }?.take(DEFAULT_SAFE_LENGTH) } @@ -139,26 +140,28 @@ class DefaultRoomLatestEventFormatter( messageType.toPlainText(permalinkParser) } is VideoMessageType -> { - messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_video)) + messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_video)) } is ImageMessageType -> { - messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_image)) + messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_image)) } is StickerMessageType -> { - messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_sticker)) + messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_sticker)) } is LocationMessageType -> { sp.getString(CommonStrings.common_shared_location) } is FileMessageType -> { - messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_file)) + messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_file)) } is AudioMessageType -> { - messageType.bestDescription.prefixWith(sp.getString(CommonStrings.common_audio)) + messageType.toPlainText(permalinkParser).prefixWith(sp.getString(CommonStrings.common_audio)) } is VoiceMessageType -> { - // In this case, do not use bestDescription, because the filename is useless, only use the caption if available. - messageType.caption?.prefixWith(sp.getString(CommonStrings.common_voice_message)) + messageType + .toPlainText(permalinkParser, "") + .takeIf { it.isNotEmpty() } + ?.prefixWith(sp.getString(CommonStrings.common_voice_message)) ?: sp.getString(CommonStrings.common_voice_message) } is OtherMessageType -> { diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt index 51fdccf256..1a04893e07 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt @@ -20,6 +20,10 @@ internal fun CharSequence.prefixWith(prefix: String): AnnotatedString { append(prefix) } append(": ") - append(this@prefixWith) + if (this@prefixWith is AnnotatedString) { + append(this@prefixWith) + } else { + append(this@prefixWith.toString()) + } } } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.kt new file mode 100644 index 0000000000..ab3fb9433d --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatter.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.libraries.eventformatter.impl + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.services.toolbox.api.strings.StringProvider + +@Inject +class RtcNotificationContentFormatter( + private val matrixClient: MatrixClient, + private val sp: StringProvider, +) { + fun format( + content: CallNotifyContent, + isDm: Boolean, + ): CharSequence { + return if (isDm) { + val isDeclined = content.declinedBy.isNotEmpty() + val isDeclinedByMe = content.declinedBy.any { matrixClient.isMe(it) } + if (isDeclinedByMe) { + sp.getString(CommonStrings.common_call_you_declined) + } else if (isDeclined) { + sp.getString(CommonStrings.common_call_declined) + } else { + sp.getString(CommonStrings.common_call_started) + } + } else { + sp.getString(CommonStrings.common_call_started) + } + } +} diff --git a/libraries/eventformatter/impl/src/main/res/values-ca/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..2caa14e8a3 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,73 @@ + + + "(la foto també ha canviat)" + "%1$s ha canviat la seva foto" + "Has canviat la teva foto" + "%1$s ha baixat a membre" + "%1$s ha baixat a moderador/a" + "%1$s han canviat el seu nom de visualització de %2$s a %3$s" + "Has canviat el teu nom de visualització de %1$s a %2$s" + "%1$s ha eliminat el seu nom de visualització (era %2$s)" + "Has eliminat el teu nom de visualització (era %1$s)" + "%1$s ha definit el seu nom de visualització a %2$s" + "Has definit el teu nom de visualització a %1$s" + "%1$s ha ascendit a administrador/a" + "%1$s ha ascendit a moderador/a" + "%1$s ha canviat la foto de la sala" + "Has canviat la foto de la sala" + "%1$s ha eliminat la foto de la sala" + "Has eliminat la foto de la sala" + "%1$s ha bandejat %2$s" + "Has bandejat %1$s" + "Has bandejat %1$s: %2$s" + "%1$s ha bandejat %2$s: %3$s" + "%1$s ha creat la sala" + "Has creat la sala" + "%1$s a convidat a %2$s" + "%1$s ha acceptat la invitació" + "Has acceptat la invitació" + "Has convidat a %1$s" + "%1$s t\'ha convidat" + "%1$s s\'ha unit a la sala" + "T\'has unit a la sala" + "%1$s ha sol·licitat unir-se" + "%1$s ha concedit l\'accés a %2$s" + "Has permès %1$s unir-se" + "Has sol·licitat unir-te" + "%1$s ha rebutjat la sol·licitud d\'unió a %2$s" + "Has rebutjat la sol·licitud d\'unió a %1$s" + "%1$s ha rebutjat la teva sol·licitud d\'unió" + "%1$s ja no es vol unir" + "Has cancel·lat la sol·licitud per unir-te" + "%1$s ha sortit de la sala" + "Has sortit de la sala" + "%1$s ha canviat el nom de la sala a: %2$s" + "Has canviat el nom de la sala a: %1$s" + "%1$s ha eliminat el nom de la sala" + "Has eliminat el nom de la sala" + "%1$s no ha fet canvis" + "No s\'han fet canvis" + "%1$s ha canviat els missatges fixats" + "Has canviat els missatges fixats" + "%1$s ha fixat un missatge" + "Has fixat un missatge" + "%1$s ha deixat de fixar un missatge" + "Has deixat de fixar un missatge" + "%1$s ha rebutjat la invitació" + "Has rebutjat la invitació" + "%1$s ha eliminat %2$s" + "Has eliminat %1$s" + "Has eliminat %1$s: %2$s" + "%1$s ha eliminat %2$s: %3$s" + "%1$s ha convidat a %2$s a la sala" + "Has convidat a %1$s a la sala" + "%1$s ha rebutjat la invitació a la sala de %2$s" + "Has rebutjat la invitació a la sala de %1$s" + "%1$s ha canviat el tema a: %2$s" + "Has canviat el tema a: %1$s" + "%1$s ha eliminat el tema de la sala" + "Has eliminat el tema de la sala" + "%1$s ha readmès %2$s" + "Has readmès %1$s" + "%1$s ha fet un canvi desconegut al seu tipus d\'usuari" + diff --git a/libraries/eventformatter/impl/src/main/res/values-pl/translations.xml b/libraries/eventformatter/impl/src/main/res/values-pl/translations.xml index e45227ad62..94e1015375 100644 --- a/libraries/eventformatter/impl/src/main/res/values-pl/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-pl/translations.xml @@ -29,7 +29,7 @@ "Zaprosiłeś %1$s" "%1$s zaprosił Cię" "%1$s dołączył do pokoju" - "Dołączyłeś(aś) do pokoju" + "Dołączyłeś do pokoju" "%1$s prosi o możliwość dołączenia" "%1$s zezwolił %2$s na dołączenie" "Zezwoliłeś %1$s na dołączenie" diff --git a/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml index e14813bb69..196e6ec3e1 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ru/translations.xml @@ -27,7 +27,7 @@ "%1$s принял(а) приглашение" "Вы приняли приглашение" "Вы пригласили %1$s" - "%1$s пригласил(а) вас" + "%1$s пригласил(а) Вас" "%1$s присоединился(ась)" "Вы присоединились к комнате" "%1$s хочет присоединиться" @@ -47,8 +47,8 @@ "Вы удалили название" "%1$s не внес(ла) изменений" "Вы не внесли изменений" - "%1$s изменил(а) закрепленные сообщения" - "Вы изменили закрепленные сообщения" + "%1$s изменил(а) закреплённые сообщения" + "Вы изменили закреплённые сообщения" "%1$s закрепил(а) сообщение" "Вы закрепили сообщение" "%1$s открепил(а) сообщение" 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 35a501c7a2..f7e5d7d7f2 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh/translations.xml @@ -45,8 +45,8 @@ "你将房间名称更改为 %1$s" "%1$s 移除了房间名称" "你移除了房间名称" - "%1$s 没有任何更改" - "你未做任何更改" + "%1$s 未产生任何更改" + "你未产生任何更改" "%1$s 更改了已置顶的消息" "你更改了已置顶的消息" "%1$s 置顶了 1 个消息" 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 38334afad5..d36f262216 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 @@ -50,16 +50,14 @@ import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config @Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class DefaultPinnedMessagesBannerFormatterTest { +class DefaultPinnedMessagesBannerFormatterTest : RobolectricTest() { private lateinit var context: Context private lateinit var fakeMatrixClient: FakeMatrixClient private lateinit var formatter: DefaultPinnedMessagesBannerFormatter 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 e1e8717c4c..44e407dcdc 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 @@ -10,6 +10,7 @@ package io.element.android.libraries.eventformatter.impl import android.content.Context import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import io.element.android.libraries.matrix.api.core.UserId @@ -50,16 +51,14 @@ import io.element.android.libraries.matrix.test.timeline.aProfileDetails import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config @Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class DefaultRoomLatestEventFormatterTest { +class DefaultRoomLatestEventFormatterTest : RobolectricTest() { private lateinit var context: Context private lateinit var fakeMatrixClient: FakeMatrixClient private lateinit var formatter: DefaultRoomLatestEventFormatter @@ -74,7 +73,8 @@ class DefaultRoomLatestEventFormatterTest { roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), - permalinkParser = FakePermalinkParser(), + rtcNotificationContentFormatter = RtcNotificationContentFormatter(fakeMatrixClient, stringProvider), + permalinkParser = FakePermalinkParser() ) } @@ -102,7 +102,14 @@ class DefaultRoomLatestEventFormatterTest { val info = ImageInfo(null, null, null, null, null, null, null) val message = createLatestEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url"))) val result = formatter.format(message, false) - val expectedBody = someoneElseId.value + ": Sticker (a sticker body)" + val expectedBody = someoneElseId.value + ": Sticker: a sticker body" + // Check we have formatting + assertThat(result is AnnotatedString).isTrue() + // And there is a bold span for the 'Sticker' part + val boldSpanStyle = (result as AnnotatedString).spanStyles.lastOrNull { it.item.fontWeight == FontWeight.Bold } + assertThat(boldSpanStyle).isNotNull() + val spanStart = someoneElseId.value.length + 2 + assertThat(boldSpanStyle!!.start..boldSpanStyle.end).isEqualTo(spanStart..spanStart + 7) assertThat(result.toString()).isEqualTo(expectedBody) } @@ -908,10 +915,18 @@ class DefaultRoomLatestEventFormatterTest { val pollContent = aPollContent() val mineContentEvent = createLatestEvent(sentByYou = true, senderDisplayName = "Alice", content = pollContent) - assertThat(formatter.format(mineContentEvent, true)).isEqualTo("Poll: Do you like polls?") + assertThat(formatter.format(mineContentEvent, true).toString()).isEqualTo("Poll: Do you like polls?") val contentEvent = createLatestEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) - assertThat(formatter.format(contentEvent, true)).isEqualTo("Poll: Do you like polls?") + assertThat(formatter.format(contentEvent, true).toString()).isEqualTo("Poll: Do you like polls?") + + val result = formatter.format(contentEvent, true) + // Check we have formatting + assertThat(result is AnnotatedString).isTrue() + // And there is a bold span for the 'Poll' part + val boldSpanStyle = (result as AnnotatedString).spanStyles.lastOrNull { it.item.fontWeight == FontWeight.Bold } + assertThat(boldSpanStyle).isNotNull() + assertThat(boldSpanStyle!!.start..boldSpanStyle.end).isEqualTo(0..4) } @Test @@ -924,6 +939,15 @@ class DefaultRoomLatestEventFormatterTest { val contentEvent = createLatestEvent(sentByYou = false, senderDisplayName = "Bob", content = pollContent) assertThat(formatter.format(contentEvent, false).toString()).isEqualTo("Bob: Poll: Do you like polls?") + + val result = formatter.format(contentEvent, false) + // Check we have formatting + assertThat(result is AnnotatedString).isTrue() + // And there is a bold span for the 'Poll' part + val boldSpanStyle = (result as AnnotatedString).spanStyles.lastOrNull { it.item.fontWeight == FontWeight.Bold } + assertThat(boldSpanStyle).isNotNull() + val spanStart = "Bob".length + 2 + assertThat(boldSpanStyle!!.start..boldSpanStyle.end).isEqualTo(spanStart..spanStart + 4) } // endregion diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt new file mode 100644 index 0000000000..cd3a8df349 --- /dev/null +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/RtcNotificationContentFormatterTest.kt @@ -0,0 +1,96 @@ +/* + * 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.eventformatter.impl + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.notification.CallIntent +import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Before +import org.junit.Test +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@Suppress("LargeClass") +class RtcNotificationContentFormatterTest : RobolectricTest() { + private lateinit var context: Context + private lateinit var fakeMatrixClient: FakeMatrixClient + private lateinit var formatter: RtcNotificationContentFormatter + + @Before + fun setup() { + context = RuntimeEnvironment.getApplication() as Context + fakeMatrixClient = FakeMatrixClient() + val stringProvider = AndroidStringProvider(context.resources) + formatter = RtcNotificationContentFormatter( + fakeMatrixClient, + stringProvider + ) + } + + @Test + @Config(qualifiers = "en") + fun `Should not display declined info in rooms`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.VIDEO, + declinedBy = listOf(A_USER_ID_2, A_USER_ID_3) + ), + false + ) + val expected = "Call started" + assertThat(result.toString()).isEqualTo(expected) + } + + @Test + @Config(qualifiers = "en") + fun `Declined by me variant`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.VIDEO, + declinedBy = listOf(fakeMatrixClient.sessionId) + ), + true + ) + val expected = "You declined a call" + assertThat(result.toString()).isEqualTo(expected) + } + + @Test + @Config(qualifiers = "en") + fun `Declined by other variant`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.VIDEO, + declinedBy = listOf(A_USER_ID_2) + ), + true + ) + val expected = "Call declined" + assertThat(result.toString()).isEqualTo(expected) + } + + @Test + @Config(qualifiers = "en") + fun `Call started in DM`() { + val result = formatter.format( + CallNotifyContent( + CallIntent.AUDIO, + declinedBy = listOf() + ), + true + ) + val expected = "Call started" + assertThat(result.toString()).isEqualTo(expected) + } +} 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 15e61f4260..0127e41c99 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 @@ -22,13 +22,6 @@ enum class FeatureFlags( override val isFinished: Boolean, override val isInLabs: Boolean = false, ) : Feature { - RoomDirectorySearch( - key = "feature.roomdirectorysearch", - title = "Room directory search", - description = "Allow user to search for public rooms in their homeserver", - defaultValue = { false }, - isFinished = false, - ), ShowBlockedUsersDetails( key = "feature.showBlockedUsersDetails", title = "Show blocked users details", @@ -100,27 +93,13 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), - SignInWithClassic( - key = "feature.signin_with_classic", - title = "Sign in with Element Classic", - description = "Allow the application to sign in to the current Element Classic account.", - defaultValue = { false }, - isFinished = false, - ), AllowBlackTheme( key = "feature.allow_black_theme", - title = "Allow black theme", + title = "Black theme", description = "Allow selecting the black appearance theme for battery saving on OLED.", defaultValue = { false }, isFinished = false, ), - LiveLocationSharing( - key = "feature.liveLocationSharing", - title = "Live location sharing", - description = "Allow sharing live location in rooms.", - defaultValue = { false }, - isFinished = false, - ), ValidateNetworkWhenSchedulingNotificationFetching( key = "feature.validate_network_when_scheduling_notification_fetching", title = "Validate internet connectivity when scheduling notification fetching", @@ -129,13 +108,6 @@ enum class FeatureFlags( 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", @@ -158,4 +130,11 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + UnreadIndicatorCount( + key = "feature.unread_indicator_count", + title = "Unread indicator count", + description = "Show the number of unread messages on the unread indicator in the room list.", + defaultValue = { false }, + isFinished = false, + ), } diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 1c70006cc8..2a326527df 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -44,14 +44,12 @@ android { } dependencies { - implementation(projects.libraries.di) - implementation(projects.libraries.androidutils) - implementation(projects.libraries.core) - implementation(projects.services.analytics.api) - implementation(libs.serialization.json) - api(projects.libraries.sessionStorage.api) implementation(libs.coroutines.core) - api(projects.libraries.architecture) + implementation(libs.serialization.json) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.sessionStorage.api) + implementation(projects.services.analytics.api) testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) 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 35fd7e8551..8f482440e8 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 @@ -26,7 +26,8 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.NotJoinedRoom import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.spaces.SpaceService @@ -52,6 +54,7 @@ import java.util.Optional interface MatrixClient { val sessionId: SessionId val deviceId: DeviceId + val sessionPaths: SessionPaths val userProfile: StateFlow val roomListService: RoomListService val spaceService: SpaceService @@ -67,6 +70,7 @@ interface MatrixClient { val sessionCoroutineScope: CoroutineScope val ignoredUsersFlow: StateFlow> val roomMembershipObserver: RoomMembershipObserver + val ownBeaconInfoUpdates: Flow suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? suspend fun getRoom(roomId: RoomId): BaseRoom? suspend fun findDM(userId: UserId): Result @@ -219,6 +223,11 @@ interface MatrixClient { */ suspend fun performDatabaseVacuum(): Result + /** + * Returns the URL of the map style configured on the server, if any. + */ + suspend fun getMapStyleUrl(): Result + /** * Resets the cached client `well-known` config by the SDK. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt index ab11374116..fde02fb2c6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt @@ -10,5 +10,5 @@ package io.element.android.libraries.matrix.api.analytics import io.element.android.libraries.matrix.api.core.SessionId fun interface GetDatabaseSizesUseCase { - suspend operator fun invoke(sessionId: SessionId): Result + operator fun invoke(sessionId: SessionId): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt index ac3b0c8e3a..f515ce8c32 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt @@ -19,7 +19,7 @@ fun BaseRoom.toAnalyticsViewRoom( val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home return ViewRoom( - isDM = info().isDirect, + isDM = info().isDm, isSpace = info().isSpace, trigger = trigger, activeSpace = activeSpace, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt index c50ec09609..d5fd6a734a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt @@ -16,6 +16,6 @@ sealed class AuthenticationException(message: String?) : Exception(message) { class InvalidServerName(message: String?) : AuthenticationException(message) class SlidingSyncVersion(message: String?) : AuthenticationException(message) class ServerUnreachable(message: String?) : AuthenticationException(message) - class Oidc(message: String?) : AuthenticationException(message) + class OAuth(message: String?) : AuthenticationException(message) class Generic(message: String?) : AuthenticationException(message) } 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 7c82668242..04d1d13593 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 @@ -37,21 +37,21 @@ interface MatrixAuthenticationService { suspend fun importCreatedSession(externalSession: ExternalSession): Result /* - * OIDC part. + * OAuth part. */ /** - * Get the Oidc url to display to the user. + * Get the OAuth url to display to the user. */ - suspend fun getOidcUrl( - prompt: OidcPrompt, + suspend fun getOAuthUrl( + prompt: OAuthPrompt, loginHint: String?, - ): Result + ): Result /** - * Cancel Oidc login sequence. + * Cancel OAuth login sequence. */ - suspend fun cancelOidcLogin(): Result + suspend fun cancelOAuthLogin(): Result /** * Set the existing data about Element Classic session, if any. @@ -68,9 +68,9 @@ interface MatrixAuthenticationService { ): Boolean /** - * Attempt to login using the [callbackUrl] provided by the Oidc page. + * Attempt to log in using the [callbackUrl] provided by the OAuth page. */ - suspend fun loginWithOidc(callbackUrl: String): Result + suspend fun loginWithOAuth(callbackUrl: String): Result suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt index aa5ed9a41d..8dcb5c4a48 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.api.auth data class MatrixHomeServerDetails( val url: String, val supportsPasswordLogin: Boolean, - val supportsOidcLogin: Boolean, + val supportsOAuthLogin: Boolean, ) { - val isSupported = supportsPasswordLogin || supportsOidcLogin + val isSupported = supportsPasswordLogin || supportsOAuthLogin } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthConfig.kt similarity index 97% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthConfig.kt index ee8b7ec50e..d3a42f42b9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthConfig.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.api.auth import io.element.android.libraries.matrix.api.BuildConfig -object OidcConfig { +object OAuthConfig { const val CLIENT_URI = BuildConfig.CLIENT_URI // Note: host must match with the host of CLIENT_URI diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthDetails.kt similarity index 94% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthDetails.kt index c4fb87e3c2..d504f891ee 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthDetails.kt @@ -12,6 +12,6 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class OidcDetails( +data class OAuthDetails( val url: String, ) : Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthPrompt.kt similarity index 81% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthPrompt.kt index 8ddad9f52e..45b4e18533 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcPrompt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthPrompt.kt @@ -8,12 +8,12 @@ package io.element.android.libraries.matrix.api.auth -sealed interface OidcPrompt { +sealed interface OAuthPrompt { /** * The Authorization Server should prompt the End-User for * reauthentication. */ - data object Login : OidcPrompt + data object Login : OAuthPrompt /** * The Authorization Server should prompt the End-User to create a user @@ -21,10 +21,10 @@ sealed interface OidcPrompt { * * Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html). */ - data object Create : OidcPrompt + data object Create : OAuthPrompt /** * An unknown value. */ - data class Unknown(val value: String) : OidcPrompt + data class Unknown(val value: String) : OAuthPrompt } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt similarity index 89% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt index ad4d862474..669d47501d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcRedirectUrlProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt @@ -8,6 +8,6 @@ package io.element.android.libraries.matrix.api.auth -interface OidcRedirectUrlProvider { +interface OAuthRedirectUrlProvider { fun provide(): String } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt index a3b567fa46..3f22244405 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/QrLoginException.kt @@ -15,7 +15,7 @@ sealed class QrLoginException : Exception() { data object Expired : QrLoginException() data object NotFound : QrLoginException() data object LinkingNotSupported : QrLoginException() - data object OidcMetadataInvalid : QrLoginException() + data object OAuthMetadataInvalid : QrLoginException() data object SlidingSyncNotAvailable : QrLoginException() data object OtherDeviceNotSignedIn : QrLoginException() data object CheckCodeAlreadySent : QrLoginException() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index aefad517dc..f82cdc1969 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -24,9 +24,15 @@ interface EncryptionService { suspend fun enableBackups(): Result /** - * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. + * Enable recovery and return the SDK-generated recovery key on success. + * Observe [enableRecoveryProgressStateFlow] for in-progress UI updates. + * + * @param waitForBackupsToUpload when true, suspends until existing room keys finish uploading. + * @param passphrase optional user-supplied passphrase. When set, the SDK derives the + * secret-storage key from it instead of a random base58 key; the passphrase can later be + * passed to [recover], and the returned base58 key should not be surfaced to the user. */ - suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result + suspend fun enableRecovery(waitForBackupsToUpload: Boolean, passphrase: String? = null): Result /** * Change the recovery and return the new recovery key. @@ -112,19 +118,19 @@ interface IdentityPasswordResetHandle : IdentityResetHandle { } /** - * A handle to reset the user's identity with an OIDC login type. + * A handle to reset the user's identity with an OAuth login type. */ -interface IdentityOidcResetHandle : IdentityResetHandle { +interface IdentityOAuthResetHandle : IdentityResetHandle { /** * The URL to open in a webview/custom tab to reset the identity. */ val url: String /** - * Reset the identity using the OIDC flow. + * Reset the identity using the OAuth flow. * * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is * called, or the identity is reset. */ - suspend fun resetOidc(): Result + suspend fun resetOAuth(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt index 52b1577bf0..565dc59614 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt @@ -15,6 +15,7 @@ sealed class ClientException(message: String, val details: String?, cause: Throw details = details, cause = cause ) + class ContentScanner(message: String, val reason: ContentScannerErrorReason) : ClientException(message, null, null) class Other(message: String, cause: Throwable? = null) : ClientException(message, null, cause) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ContentScannerErrorReason.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ContentScannerErrorReason.kt new file mode 100644 index 0000000000..5c32b3989b --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ContentScannerErrorReason.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.api.exception + +enum class ContentScannerErrorReason { + MCS_MALFORMED_JSON, + MCS_MEDIA_FAILED_TO_DECRYPT, + M_MISSING_TOKEN, + M_UNKNOWN_TOKEN, + M_NOT_FOUND, + MCS_MEDIA_NOT_CLEAN, + MCS_MIME_TYPE_FORBIDDEN, + MCS_BAD_DECRYPTION, + M_UNKNOWN, + MCS_MEDIA_REQUEST_FAILED; + + companion object; + + fun isDangerous(): Boolean = when (this) { + MCS_MEDIA_NOT_CLEAN, MCS_MIME_TYPE_FORBIDDEN -> true + else -> false + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt index 0c261cdd1a..eb2d49b5b0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt @@ -16,8 +16,12 @@ interface LinkMobileHandler { sealed interface LinkMobileStep { data object Uninitialized : LinkMobileStep + + // Internal application step, for the UI + data object CreatingQrCode : LinkMobileStep data object Starting : LinkMobileStep data class QrReady(val data: String) : LinkMobileStep + data object QrRotating : LinkMobileStep data class WaitingForAuth(val verificationUri: String) : LinkMobileStep data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep data class Error(val errorType: ErrorType) : LinkMobileStep diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt index 4d8ce8afb4..7ae1ca4886 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt @@ -21,7 +21,7 @@ interface NotificationSettingsService { val notificationSettingsChangeFlow: SharedFlow suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result - suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result + suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isDM: Boolean): Result suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result suspend fun muteRoom(roomId: RoomId): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oauth/AccountManagementAction.kt similarity index 91% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oauth/AccountManagementAction.kt index e1c7764e58..6dd0f6e53c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oidc/AccountManagementAction.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/oauth/AccountManagementAction.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.api.oidc +package io.element.android.libraries.matrix.api.oauth import io.element.android.libraries.matrix.api.core.DeviceId diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/paths/SessionPaths.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/paths/SessionPaths.kt new file mode 100644 index 0000000000..d481316f9d --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/paths/SessionPaths.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.paths + +import java.io.File + +data class SessionPaths( + val fileDirectory: File, + val cacheDirectory: File, +) { + fun deleteRecursively() { + fileDirectory.deleteRecursively() + cacheDirectory.deleteRecursively() + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt index 8a557f4b70..97a3cabd20 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/SetHttpPusherData.kt @@ -17,4 +17,5 @@ data class SetHttpPusherData( val profileTag: String?, val lang: String, val defaultPayload: String, + val append: Boolean, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index 589f88e9fd..d49343f998 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -61,13 +61,12 @@ interface BaseRoom : Closeable { */ fun info(): RoomInfo = roomInfoFlow.value - fun predecessorRoom(): PredecessorRoom? - /** - * A one-to-one is a room with exactly 2 members. - * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). + * Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo]. */ - val isOneToOne: Boolean get() = info().activeMembersCount == 2L + fun isDm() = roomInfoFlow.value.isDm + + fun predecessorRoom(): PredecessorRoom? /** * Try to load the room members and update the membersFlow. @@ -158,11 +157,6 @@ interface BaseRoom : Closeable { */ suspend fun setUnreadFlag(isUnread: Boolean): Result - /** - * Clear the event cache storage for the current room. - */ - suspend fun clearEventCacheStorage(): Result - /** * Get the permalink for the room. */ 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 32a6f2e409..6d8325427b 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 @@ -196,9 +196,9 @@ interface JoinedRoom : BaseRoom { /** * Start sharing live location in this room. * @param durationMillis How long to share location (in milliseconds). - * @return Result indicating success or failure. + * @return Result containing the [EventId] of the beacon state event on success or an error on failure. */ - suspend fun startLiveLocationShare(durationMillis: Long): Result + suspend fun startLiveLocationShare(durationMillis: Long): Result /** * Stop sharing live location in this room. @@ -212,4 +212,11 @@ interface JoinedRoom : BaseRoom { * @return Result indicating success or failure. */ suspend fun sendLiveLocation(geoUri: String): Result + + /** + * Sets the display name of the current user within this room. + * This is different from the global setDisplayName which updates + * the user's display name across all of their rooms. + */ + suspend fun setOwnMemberDisplayName(displayName: String): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt index 5247e402a6..e590976cab 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomInfo.kt @@ -29,6 +29,7 @@ data class RoomInfo( val avatarUrl: String?, val isPublic: Boolean?, val isDirect: Boolean, + val isDm: Boolean, val isEncrypted: Boolean?, val joinRule: JoinRule?, val isSpace: Boolean, @@ -78,6 +79,7 @@ data class RoomInfo( val privilegedCreatorRole: Boolean, val isLowPriority: Boolean, val activeCallIntentConsensus: CallIntentConsensus, + val fullyReadEventId: EventId?, ) { val aliases: List get() = listOfNotNull(canonicalAlias) + alternativeAliases diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt deleted file mode 100644 index f33319e2ee..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheck.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.api.room - -import kotlinx.coroutines.flow.first - -/** - * Returns whether the room with the provided info is a DM. - * A DM is a room with at most 2 active members (one of them may have left). - * - * @param isDirect true if the room is direct - * @param activeMembersCount the number of active members in the room (joined or invited) - */ -fun isDm(isDirect: Boolean, activeMembersCount: Int): Boolean { - return isDirect && activeMembersCount <= 2 -} - -/** - * Returns whether the [BaseRoom] is a DM, with an updated state from the latest [RoomInfo]. - */ -suspend fun BaseRoom.isDm() = roomInfoFlow.first().isDm - -/** - * Returns whether the [RoomInfo] is from a DM. - */ -val RoomInfo.isDm get() = isDm(isDirect, activeMembersCount.toInt()) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 0b3d7071c8..abf685a38b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -22,6 +22,7 @@ data class RoomMember( val isIgnored: Boolean, val role: Role, val membershipChangeReason: String?, + val isServiceMember: Boolean, ) { /** * Role of the RoomMember, based on its [powerLevel]. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt index 1c35fab7d0..2c93fa94f0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembersState.kt @@ -40,5 +40,5 @@ fun RoomMembersState.activeRoomMembers(): List { fun RoomMembersState.getDirectRoomMember(roomInfo: RoomInfo, sessionId: SessionId): RoomMember? { return roomMembers() ?.takeIf { roomInfo.isDm } - ?.find { it.userId != sessionId && it.membership.isActive() } + ?.find { !it.isServiceMember && it.userId != sessionId && it.membership.isActive() } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconId.kt new file mode 100644 index 0000000000..358dcd98d1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconId.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.libraries.matrix.api.room.location + +import io.element.android.libraries.matrix.api.core.EventId + +typealias BeaconId = EventId diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconInfoUpdate.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconInfoUpdate.kt new file mode 100644 index 0000000000..0b7e9b0f44 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/BeaconInfoUpdate.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.location + +import io.element.android.libraries.matrix.api.core.RoomId + +data class BeaconInfoUpdate( + val roomId: RoomId, + val beaconId: BeaconId, + val isLive: Boolean, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationException.kt new file mode 100644 index 0000000000..9b53603042 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationException.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.matrix.api.room.location + +sealed class LiveLocationException(message: String?) : Exception(message) { + class NotLive : LiveLocationException("The beacon event has expired.") + class Network : LiveLocationException("Network error") + class Other(val exception: Exception) : LiveLocationException(exception.message) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationShare.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationShare.kt index 3f9c108dc7..4d8bc4638a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationShare.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/location/LiveLocationShare.kt @@ -21,6 +21,8 @@ data class LiveLocationShare( val startTimestamp: Long, /** The timestamp when location sharing ends, in milliseconds. */ val endTimestamp: Long, + /** The event id from the beacon info. */ + val beaconId: BeaconId ) data class LastLocation( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt index e8f88ed86d..6606465389 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPowerLevelsValues.kt @@ -19,4 +19,6 @@ data class RoomPowerLevelsValues( val roomAvatar: Long, val roomTopic: Long, val spaceChild: Long, + val beacon: Long, + val beaconInfo: Long, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt index 6db326c854..3441a4be3a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt @@ -12,7 +12,6 @@ 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.room.CurrentUserMembership -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.coroutines.flow.Flow diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt index aca093eab6..10ca376105 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomSummary.kt @@ -21,5 +21,5 @@ data class RoomSummary( is LatestEventValue.Remote -> latestEvent.timestamp is LatestEventValue.RoomInvite -> latestEvent.timestamp } - val isOneToOne get() = info.activeMembersCount == 2L + val isDm = info.isDm } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt index 6a72577760..e22b0a5c9c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt @@ -38,6 +38,7 @@ data class SpaceRoom( */ val via: ImmutableList, val isDirect: Boolean?, + val isDm: Boolean?, ) { val isSpace = roomType == RoomType.Space diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index d91d404a0b..f323b316f7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -119,7 +119,8 @@ data class LiveLocationContent( data object LegacyCallInviteContent : EventContent data class CallNotifyContent( - val callIntent: CallIntent + val callIntent: CallIntent, + val declinedBy: List ) : EventContent data object UnknownContent : EventContent diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index 4d2e786038..38a00d833d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -33,7 +33,7 @@ interface SessionVerificationService { /** * Request verification of the current session. */ - suspend fun requestCurrentSessionVerification() + suspend fun requestDeviceVerification() /** * Request verification of the user with the given [userId]. @@ -56,9 +56,9 @@ interface SessionVerificationService { suspend fun declineVerification() /** - * Starts the verification of the unverified session from another device. + * Transition the current verification request into a SAS verification flow. */ - suspend fun startVerification() + suspend fun startSasVerification() /** * Returns the verification service state to the initial step. diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetailsTest.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetailsTest.kt index d4b360ef53..9babb7a738 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetailsTest.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetailsTest.kt @@ -16,7 +16,7 @@ class MatrixHomeServerDetailsTest { @Test fun `if homeserver supports oidc, then it is supported`() { val sut = aMatrixHomeServerDetails( - supportsOidcLogin = true, + supportsOAuthLogin = true, supportsPasswordLogin = false, ) assertThat(sut.isSupported).isTrue() @@ -25,7 +25,7 @@ class MatrixHomeServerDetailsTest { @Test fun `if homeserver supports password, then it is supported`() { val sut = aMatrixHomeServerDetails( - supportsOidcLogin = false, + supportsOAuthLogin = false, supportsPasswordLogin = true, ) assertThat(sut.isSupported).isTrue() @@ -34,7 +34,7 @@ class MatrixHomeServerDetailsTest { @Test fun `if homeserver supports both, then it is supported`() { val sut = aMatrixHomeServerDetails( - supportsOidcLogin = true, + supportsOAuthLogin = true, supportsPasswordLogin = true, ) assertThat(sut.isSupported).isTrue() @@ -43,7 +43,7 @@ class MatrixHomeServerDetailsTest { @Test fun `if homeserver supports none, then it is not supported`() { val sut = aMatrixHomeServerDetails( - supportsOidcLogin = false, + supportsOAuthLogin = false, supportsPasswordLogin = false, ) assertThat(sut.isSupported).isFalse() diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheckTest.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheckTest.kt deleted file mode 100644 index 1461d28fd9..0000000000 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/room/RoomIsDmCheckTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.api.room - -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class RoomIsDmCheckTest { - @Test - fun `a room is a DM only if it has at most 2 members and is direct`() { - val isDirect = true - val activeMembersCount = 2 - - val isDm = isDm(isDirect, activeMembersCount) - - assertThat(isDm).isTrue() - } - - @Test - fun `a room can be a DM if it has also a single active user`() { - val isDirect = true - val activeMembersCount = 1 - - val isDm = isDm(isDirect, activeMembersCount) - - assertThat(isDm).isTrue() - } - - @Test - fun `a room is not a DM if it's not direct`() { - val isDirect = false - val activeMembersCount = 2 - - val isDm = isDm(isDirect, activeMembersCount) - - assertThat(isDm).isFalse() - } - - @Test - fun `a room is not a DM if it has more than 2 active users`() { - val isDirect = true - val activeMembersCount = 3 - - val isDm = isDm(isDirect, activeMembersCount) - - assertThat(isDm).isFalse() - } -} diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 67386cc592..2499b720e3 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -31,22 +31,26 @@ dependencies { implementation(projects.libraries.rustlsTls) implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) implementation(projects.libraries.di) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.workmanager.api) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) api(projects.libraries.matrix.api) implementation(projects.libraries.core) - implementation("net.java.dev.jna:jna:5.18.1@aar") + implementation(variantOf(libs.jna) { artifactType("aar") }) implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) implementation(libs.kotlinx.collections.immutable) testCommonDependencies(libs) + testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.preferences.test) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt index 690995ba77..a5c69bf831 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegate.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.impl -import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.impl.core.SdkBackgroundTaskError import io.element.android.libraries.matrix.impl.mapper.toSessionData @@ -19,6 +19,7 @@ import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.ClientSessionDelegate import org.matrix.rustcomponents.sdk.Session @@ -41,14 +42,10 @@ class RustClientSessionDelegate( private val sessionStore: SessionStore, private val appCoroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, - coroutineDispatchers: CoroutineDispatchers, ) : ClientSessionDelegate, ClientDelegate { // Used to ensure several calls to `didReceiveAuthError` don't trigger multiple logouts private val isLoggingOut = AtomicBoolean(false) - // To make sure only one coroutine affecting the token persistence can run at a time - private val updateTokensDispatcher = coroutineDispatchers.io.limitedParallelism(1) - // This Client needs to be set up as soon as possible so `didReceiveAuthError` can work properly. private var client: WeakReference = WeakReference(null) @@ -66,11 +63,24 @@ class RustClientSessionDelegate( this.client.clear() } + // This always runs on a background thread, so we *can* do blocking calls here, although we should avoid doing heavy work override fun saveSessionInKeychain(session: Session) { - appCoroutineScope.launch(updateTokensDispatcher) { - val existingData = sessionStore.getSession(session.userId) ?: return@launch + Timber.tag(loggerTag.value).i("Saving new session info for user ${session.userId} after a token refresh") + runCatchingExceptions { + val existingData = runBlocking { sessionStore.getSession(session.userId) } ?: return + + if (existingData.accessToken == session.accessToken) { + Timber.tag(loggerTag.value).e("Access token is the same as the one already stored, this should not happen after a token refresh!") + return + } + + if (existingData.refreshToken == session.refreshToken) { + Timber.tag(loggerTag.value).e("Refresh token is the same as the one already stored, this should not happen after a token refresh!") + return + } + val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens() - Timber.tag(loggerTag.value).d( + Timber.tag(loggerTag.value).i( "Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " + "Was token valid: ${existingData.isTokenValid}" ) @@ -80,28 +90,27 @@ class RustClientSessionDelegate( passphrase = existingData.passphrase, sessionPaths = existingData.getSessionPaths(), ) - sessionStore.updateData(newData) - Timber.tag(loggerTag.value).d("Saved new session data with access token: '$anonymizedAccessToken'.") - }.invokeOnCompletion { - if (it != null) { - Timber.tag(loggerTag.value).e(it, "Failed to save new session data.") - } + runBlocking { sessionStore.updateData(newData) } + Timber.tag(loggerTag.value).i("Saved new session data.") + }.onFailure { + Timber.tag(loggerTag.value).e(it, "Failed to save new session data.") } } + // This always runs on a background thread, so we *can* do blocking calls here, although we should avoid doing heavy work override fun didReceiveAuthError(isSoftLogout: Boolean) { - Timber.tag(loggerTag.value).w("didReceiveAuthError(isSoftLogout=$isSoftLogout)") - if (isLoggingOut.getAndSet(true).not()) { - Timber.tag(loggerTag.value).v("didReceiveAuthError -> do the cleanup") - // TODO handle isSoftLogout parameter. - appCoroutineScope.launch(updateTokensDispatcher) { + runCatchingExceptions { + Timber.tag(loggerTag.value).w("didReceiveAuthError(isSoftLogout=$isSoftLogout)") + if (isLoggingOut.getAndSet(true).not()) { + Timber.tag(loggerTag.value).v("didReceiveAuthError -> do the cleanup") + // TODO handle isSoftLogout parameter. val currentClient = client.get() if (currentClient == null) { Timber.tag(loggerTag.value).w("didReceiveAuthError -> no client, exiting") isLoggingOut.set(false) - return@launch + return } - val existingData = sessionStore.getSession(currentClient.sessionId.value) + val existingData = runBlocking { sessionStore.getSession(currentClient.sessionId.value) } val (anonymizedAccessToken, anonymizedRefreshToken) = existingData.anonymizedTokens() Timber.tag(loggerTag.value).d( "Removing session data with access token '$anonymizedAccessToken' " + @@ -110,19 +119,17 @@ class RustClientSessionDelegate( if (existingData != null) { // Set isTokenValid to false val newData = existingData.copy(isTokenValid = false) - sessionStore.updateData(newData) + runBlocking { sessionStore.updateData(newData) } Timber.tag(loggerTag.value).d("Invalidated session data with access token: '$anonymizedAccessToken'.") } else { Timber.tag(loggerTag.value).d("No session data found.") } - currentClient.logout(userInitiated = false, ignoreSdkError = true) - }.invokeOnCompletion { - if (it != null) { - Timber.tag(loggerTag.value).e(it, "Failed to remove session data.") - } + appCoroutineScope.launch { currentClient.logout(userInitiated = false, ignoreSdkError = true) } + } else { + Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up") } - } else { - Timber.tag(loggerTag.value).v("didReceiveAuthError -> already cleaning up") + }.onFailure { + Timber.tag(loggerTag.value).e(it, "Failed to remove session data.") } } 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 bb6806b5d4..9167b3e2f3 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 @@ -31,7 +31,8 @@ import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -59,7 +60,7 @@ import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService -import io.element.android.libraries.matrix.impl.oidc.toRustAction +import io.element.android.libraries.matrix.impl.oauth.toRustAction import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.GetRoomResult import io.element.android.libraries.matrix.impl.room.NotJoinedRustRoom @@ -70,6 +71,7 @@ import io.element.android.libraries.matrix.impl.room.RustRoomFactory import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory 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.location.map import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService import io.element.android.libraries.matrix.impl.roomdirectory.map @@ -80,7 +82,6 @@ import io.element.android.libraries.matrix.impl.spaces.RustSpaceService import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.map import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper -import io.element.android.libraries.matrix.impl.util.SessionPathsProvider import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService @@ -113,6 +114,8 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.AuthData import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails +import org.matrix.rustcomponents.sdk.BeaconInfoListener +import org.matrix.rustcomponents.sdk.BeaconInfoUpdate import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.IgnoredUsersListener @@ -134,6 +137,7 @@ import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService class RustMatrixClient( + override val sessionPaths: SessionPaths, private val innerClient: Client, private val sessionStore: SessionStore, private val sessionDelegate: RustClientSessionDelegate, @@ -184,8 +188,6 @@ class RustMatrixClient( sessionDispatcher = sessionDispatcher, ) - private val sessionPathsProvider = SessionPathsProvider(sessionStore) - private val roomSyncSubscriber: RoomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers) override val roomListService: RoomListService = RustRoomListService( @@ -207,6 +209,15 @@ class RustMatrixClient( analyticsService = analyticsService, ) + override val ownBeaconInfoUpdates = mxCallbackFlow { + val listener = object : BeaconInfoListener { + override fun onUpdate(update: BeaconInfoUpdate) { + trySend(update.map()) + } + } + innerClient.subscribeToOwnBeaconInfoUpdates(listener) + } + override val sessionVerificationService = RustSessionVerificationService( client = innerClient, isSyncServiceReady = syncService.syncState.map { it == SyncState.Running }, @@ -796,6 +807,12 @@ class RustMatrixClient( } } + override suspend fun getMapStyleUrl(): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.tileServer()?.mapStyleUrl + } + } + override suspend fun resetWellKnownConfig(): Result { return runCatchingExceptions { Timber.d("Resetting well-known config for session $sessionId") @@ -806,17 +823,16 @@ class RustMatrixClient( private suspend fun getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { - val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L - val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles() + val cacheSize = sessionPaths.cacheDirectory.getSizeOfFiles() if (includeCryptoDb) { - cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles() + cacheSize + sessionPaths.fileDirectory.getSizeOfFiles() } else { cacheSize + listOf( "matrix-sdk-state.sqlite3", "matrix-sdk-state.sqlite3-shm", "matrix-sdk-state.sqlite3-wal", ).map { fileName -> - File(sessionDirectory.fileDirectory, fileName) + File(sessionPaths.fileDirectory, fileName) }.sumOf { file -> file.length() } @@ -825,7 +841,7 @@ class RustMatrixClient( private suspend fun deleteSessionDirectory() = withContext(sessionDispatcher) { // Delete all the files for this session - sessionPathsProvider.provides(sessionId)?.deleteRecursively() + sessionPaths.deleteRecursively() } private fun scheduleDatabaseVacuum() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 933298bc6c..fae99b75c8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl import dev.zacsweers.metro.Inject +import io.element.android.libraries.androidutils.crypto.ClientSecret import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.ByteUnit import io.element.android.libraries.core.data.megaBytes @@ -16,9 +17,8 @@ import io.element.android.libraries.di.CacheDirectory 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.paths.SessionPaths import io.element.android.libraries.matrix.impl.analytics.UtdTracker -import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider -import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory @@ -41,6 +41,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncVersion import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder import org.matrix.rustcomponents.sdk.use import timber.log.Timber +import uniffi.matrix_sdk_base.DmRoomDefinition import uniffi.matrix_sdk_base.MediaRetentionPolicy import uniffi.matrix_sdk_crypto.CollectStrategy import uniffi.matrix_sdk_crypto.DecryptionSettings @@ -58,7 +59,6 @@ class RustMatrixClientFactory( private val sessionStore: SessionStore, private val userAgentProvider: UserAgentProvider, private val proxyProvider: ProxyProvider, - private val userCertificatesProvider: UserCertificatesProvider, private val clock: SystemClock, private val analyticsService: AnalyticsService, private val featureFlagService: FeatureFlagService, @@ -71,13 +71,14 @@ class RustMatrixClientFactory( sessionStore = sessionStore, appCoroutineScope = appCoroutineScope, analyticsService = analyticsService, - coroutineDispatchers = coroutineDispatchers ) suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { + // This secret is called 'passphrase' for historical reasons, but it can be a raw key or an actual passphrase + val clientSecret = sessionData.passphrase?.let(ClientSecret::fromString) val client = getBaseClientBuilder( sessionPaths = sessionData.getSessionPaths(), - passphrase = sessionData.passphrase, + clientSecret = clientSecret, slidingSyncType = ClientBuilderSlidingSync.Restored, ) .homeserverUrl(sessionData.homeserverUrl) @@ -99,10 +100,10 @@ class RustMatrixClientFactory( client.restoreSession(sessionData.toSession()) - create(client) + create(client, sessionData) } - suspend fun create(client: Client): RustMatrixClient { + suspend fun create(client: Client, sessionData: SessionData): RustMatrixClient { val (anonymizedAccessToken, anonymizedRefreshToken) = client.session().anonymizedTokens() // Must be called before creating the sync service, timelines etc. @@ -118,6 +119,7 @@ class RustMatrixClientFactory( .finish() return RustMatrixClient( + sessionPaths = sessionData.getSessionPaths(), innerClient = client, sessionStore = sessionStore, appCoroutineScope = appCoroutineScope, @@ -131,24 +133,23 @@ class RustMatrixClientFactory( analyticsService = analyticsService, workManagerScheduler = workManagerScheduler, ).also { - Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") + Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") } } internal suspend fun getBaseClientBuilder( sessionPaths: SessionPaths, - passphrase: String?, + clientSecret: ClientSecret?, slidingSyncType: ClientBuilderSlidingSync, ): ClientBuilder { return clientBuilderProvider.provide() .run { sqliteStoreBuilderProvider.provide(sessionPaths) - .passphrase(passphrase) + .secret(clientSecret) .setupClientBuilder(this) } .setSessionDelegate(sessionDelegate) .userAgent(userAgentProvider.provide()) - .addRootCertificates(userCertificatesProvider.provides()) .autoEnableBackups(true) .autoEnableCrossSigning(true) .roomKeyRecipientStrategy( @@ -169,6 +170,7 @@ class RustMatrixClientFactory( ) .enableShareHistoryOnInvite(true) .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false) + .dmRoomDefinition(DmRoomDefinition.TWO_MEMBERS) .requestConfig( RequestConfig( timeout = 30_000uL, @@ -214,5 +216,5 @@ fun SessionData.toSession() = Session( deviceId = deviceId, homeserverUrl = homeserverUrl, slidingSyncVersion = SlidingSyncVersion.NATIVE, - oidcData = oidcData, + oauthData = oAuthData, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/DefaultGetDatabaseSizesUseCase.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/DefaultGetDatabaseSizesUseCase.kt index 8973483132..8dba525788 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/DefaultGetDatabaseSizesUseCase.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/DefaultGetDatabaseSizesUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. + * 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. @@ -9,19 +9,53 @@ package io.element.android.libraries.matrix.impl.analytics import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.data.bytes import io.element.android.libraries.matrix.api.MatrixClientProvider 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 java.io.File +import kotlin.math.max +/** + * An implementation of [GetDatabaseSizesUseCase] that manually calculates the database sizes by checking the file system. + * This is not as accurate as the SDK's internal method, but is used as a fallback for Sentry's non-suspendable hook where + * calling the SDK's suspendable method blocks the main thread and causes ANRs. + */ @ContributesBinding(AppScope::class) class DefaultGetDatabaseSizesUseCase( private val clientProvider: Lazy, ) : GetDatabaseSizesUseCase { - override suspend fun invoke(sessionId: SessionId): Result { + override fun invoke(sessionId: SessionId): Result { val client = clientProvider.value.getOrNull(sessionId) ?: return Result.failure(IllegalArgumentException("No MatrixClient for session $sessionId")) - return client.getDatabaseSizes() + val fileDir = client.sessionPaths.fileDirectory + val cacheDir = client.sessionPaths.cacheDirectory + + val cryptoSize = getDatabaseSize(fileDir, "matrix-sdk-crypto") + val stateSize = getDatabaseSize(fileDir, "matrix-sdk-state") + + val eventCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-event-cache") + val mediaCacheSize = getDatabaseSize(cacheDir, "matrix-sdk-media") + + return Result.success(SdkStoreSizes( + stateStore = stateSize.bytes, + cryptoStore = cryptoSize.bytes, + eventCacheStore = eventCacheSize.bytes, + mediaStore = mediaCacheSize.bytes, + )) + } + + private fun getDatabaseSize(baseDirectory: File, databaseFileName: String): Long { + // Check the sizes of both the main database file and the -wal file, and return the max of the two: + // The -wal file can contain additional data not yet merged into the main file, becoming larger than it. + val databaseFile = File(baseDirectory, "$databaseFileName.sqlite3") + val walFile = File(baseDirectory, "$databaseFileName.sqlite3-wal") + + val databaseSize = if (databaseFile.exists()) databaseFile.length() else 0L + val walSize = if (walFile.exists()) walFile.length() else 0L + + return max(databaseSize, walSize) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt index 263ce15bd3..8a0a246c22 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt @@ -11,7 +11,6 @@ package io.element.android.libraries.matrix.impl.analytics import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.RoomInfo -import io.element.android.libraries.matrix.api.room.isDm import kotlinx.coroutines.flow.first private fun Long.toAnalyticsRoomSize(): JoinedRoom.RoomSize { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index ebe0c5e4e8..fac5227f6a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.matrix.rustcomponents.sdk.ClientBuildException -import org.matrix.rustcomponents.sdk.OidcException +import org.matrix.rustcomponents.sdk.OAuthException fun Throwable.mapAuthenticationException(): AuthenticationException { return when (this) { @@ -28,13 +28,14 @@ fun Throwable.mapAuthenticationException(): AuthenticationException { } is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message) + is ClientBuildException.InvalidRawKey -> AuthenticationException.Generic(message) } - is OidcException -> when (this) { - is OidcException.Generic -> AuthenticationException.Oidc(message) - is OidcException.CallbackUrlInvalid -> AuthenticationException.Oidc(message) - is OidcException.Cancelled -> AuthenticationException.Oidc(message) - is OidcException.MetadataInvalid -> AuthenticationException.Oidc(message) - is OidcException.NotSupported -> AuthenticationException.Oidc(message) + is OAuthException -> when (this) { + is OAuthException.Generic -> AuthenticationException.OAuth(message) + is OAuthException.CallbackUrlInvalid -> AuthenticationException.OAuth(message) + is OAuthException.Cancelled -> AuthenticationException.OAuth(message) + is OAuthException.MetadataInvalid -> AuthenticationException.OAuth(message) + is OAuthException.NotSupported -> AuthenticationException.OAuth(message) } else -> AuthenticationException.Generic(message) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt index acf96d69d4..810b8d8e0d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt @@ -15,6 +15,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use { MatrixHomeServerDetails( url = url(), supportsPasswordLogin = supportsPasswordLogin(), - supportsOidcLogin = supportsOidcLogin(), + supportsOAuthLogin = supportsOauthLogin(), ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProvider.kt new file mode 100644 index 0000000000..d80dea2e7f --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2023-2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.auth.OAuthConfig +import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider +import org.matrix.rustcomponents.sdk.OAuthConfiguration + +@Inject +class OAuthConfigurationProvider( + private val buildMeta: BuildMeta, + private val oAuthRedirectUrlProvider: OAuthRedirectUrlProvider, +) { + fun get(): OAuthConfiguration = OAuthConfiguration( + clientName = buildMeta.applicationName, + redirectUri = oAuthRedirectUrlProvider.provide(), + clientUri = OAuthConfig.CLIENT_URI, + logoUri = OAuthConfig.LOGO_URI, + tosUri = OAuthConfig.TOS_URI, + policyUri = OAuthConfig.POLICY_URI, + staticRegistrations = OAuthConfig.STATIC_REGISTRATIONS, + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt deleted file mode 100644 index 6f9dd67b12..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProvider.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.auth - -import dev.zacsweers.metro.Inject -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.matrix.api.auth.OidcConfig -import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider -import org.matrix.rustcomponents.sdk.OidcConfiguration - -@Inject -class OidcConfigurationProvider( - private val buildMeta: BuildMeta, - private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, -) { - fun get(): OidcConfiguration = OidcConfiguration( - clientName = buildMeta.applicationName, - redirectUri = oidcRedirectUrlProvider.provide(), - clientUri = OidcConfig.CLIENT_URI, - logoUri = OidcConfig.LOGO_URI, - tosUri = OidcConfig.TOS_URI, - policyUri = OidcConfig.POLICY_URI, - staticRegistrations = OidcConfig.STATIC_REGISTRATIONS, - ) -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt index e21d8d94c6..e97ddbee84 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcPrompt.kt @@ -8,13 +8,13 @@ package io.element.android.libraries.matrix.impl.auth -import io.element.android.libraries.matrix.api.auth.OidcPrompt -import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt +import io.element.android.libraries.matrix.api.auth.OAuthPrompt +import org.matrix.rustcomponents.sdk.OAuthPrompt as RustOAuthPrompt -internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { +internal fun OAuthPrompt.toRustPrompt(): RustOAuthPrompt { return when (this) { - OidcPrompt.Login -> RustOidcPrompt.Unknown("consent") - OidcPrompt.Create -> RustOidcPrompt.Create - is OidcPrompt.Unknown -> RustOidcPrompt.Unknown(value) + OAuthPrompt.Login -> RustOAuthPrompt.Unknown("consent") + OAuthPrompt.Create -> RustOAuthPrompt.Create + is OAuthPrompt.Unknown -> RustOAuthPrompt.Unknown(value) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt index 0603fddec4..42cbd14cb3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt @@ -13,26 +13,23 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import io.element.android.libraries.matrix.impl.ClientBuilderProvider -import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import timber.log.Timber @ContributesBinding(AppScope::class) class RustHomeServerLoginCompatibilityChecker( private val clientBuilderProvider: ClientBuilderProvider, - private val userCertificatesProvider: UserCertificatesProvider, ) : HomeServerLoginCompatibilityChecker { override suspend fun check(url: String): Result = runCatchingExceptions { clientBuilderProvider.provide() .inMemoryStore() .serverNameOrHomeserverUrl(url) - .addRootCertificates(userCertificatesProvider.provides()) .build() .use { it.homeserverLoginDetails() } .use { - Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") - it.supportsOidcLogin() || it.supportsPasswordLogin() + Timber.d("Homeserver $url | OAuth: ${it.supportsOauthLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") + it.supportsOauthLogin() || it.supportsPasswordLogin() } } } 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 9fe7c7cd1f..639031334e 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 @@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.impl.auth import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.androidutils.crypto.ClientSecret import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions @@ -19,14 +21,15 @@ 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 -import io.element.android.libraries.matrix.api.auth.OidcPrompt +import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OAuthPrompt import io.element.android.libraries.matrix.api.auth.SessionRestorationException 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.paths.SessionPaths 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 @@ -34,9 +37,8 @@ import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep import io.element.android.libraries.matrix.impl.exception.mapClientException -import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator +import io.element.android.libraries.matrix.impl.keys.SecretGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData -import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory import io.element.android.libraries.matrix.impl.toSession import io.element.android.libraries.sessionstorage.api.LoginType @@ -64,15 +66,16 @@ class RustMatrixAuthenticationService( private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, private val rustMatrixClientFactory: RustMatrixClientFactory, - private val passphraseGenerator: PassphraseGenerator, - private val oidcConfigurationProvider: OidcConfigurationProvider, + private val secretGenerator: SecretGenerator, + private val oAuthConfigurationProvider: OAuthConfigurationProvider, + private val enterpriseService: EnterpriseService, ) : 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() + private val pendingKey by lazy { getDatabaseKey() } // Need to keep a copy of the current session path to eventually delete it. // Ideally it would be possible to get the sessionPath from the Client to avoid doing this. @@ -113,12 +116,9 @@ class RustMatrixAuthenticationService( } } - private fun getDatabasePassphrase(): String? { - val passphrase = passphraseGenerator.generatePassphrase() - if (passphrase != null) { - Timber.w("New sessions will be encrypted with a passphrase") - } - return passphrase + private fun getDatabaseKey(): ClientSecret { + Timber.d("New sessions will be encrypted with a raw key") + return secretGenerator.generateKey() } override suspend fun setHomeserver(homeserver: String): Result = @@ -157,10 +157,10 @@ class RustMatrixAuthenticationService( .toSessionData( isTokenValid = true, loginType = LoginType.PASSWORD, - passphrase = pendingPassphrase, + passphrase = pendingKey.formattedAsString(), sessionPaths = currentSessionPaths, ) - val matrixClient = rustMatrixClientFactory.create(client) + val matrixClient = rustMatrixClientFactory.create(client, sessionData) newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.addSession(sessionData) @@ -229,13 +229,13 @@ class RustMatrixAuthenticationService( val sessionData = externalSession.toSessionData( isTokenValid = true, loginType = LoginType.PASSWORD, - passphrase = pendingPassphrase, + passphrase = pendingKey.formattedAsString(), sessionPaths = currentSessionPaths, ) // We restore the client using the just retrieved session data client.restoreSession(sessionData.toSession()) - val matrixClient = rustMatrixClientFactory.create(client) + val matrixClient = rustMatrixClientFactory.create(client, sessionData) // We wait for the verification state to be known matrixClient.waitForKnownVerificationState() @@ -253,15 +253,15 @@ class RustMatrixAuthenticationService( private var pendingOAuthAuthorizationData: OAuthAuthorizationData? = null - override suspend fun getOidcUrl( - prompt: OidcPrompt, + override suspend fun getOAuthUrl( + prompt: OAuthPrompt, loginHint: String?, - ): Result { + ): Result { return withContext(coroutineDispatchers.io) { runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") - val oAuthAuthorizationData = client.urlForOidc( - oidcConfiguration = oidcConfigurationProvider.get(), + val oAuthAuthorizationData = client.urlForOauth( + oauthConfiguration = oAuthConfigurationProvider.get(), prompt = prompt.toRustPrompt(), loginHint = loginHint, // If we want to restore a previous session for which we have encryption keys, we can pass the deviceId here. At the moment, we don't @@ -269,24 +269,30 @@ class RustMatrixAuthenticationService( additionalScopes = emptyList(), ) val url = oAuthAuthorizationData.loginUrl() + .let { + enterpriseService.tweakMasUrl( + url = it, + homeserver = client.server() ?: client.homeserver(), + ) + } pendingOAuthAuthorizationData = oAuthAuthorizationData - OidcDetails(url) + OAuthDetails(url) }.mapFailure { failure -> - Timber.e(failure, "Failed to get OIDC URL") + Timber.e(failure, "Failed to get OAuth URL") failure.mapAuthenticationException() } } } - override suspend fun cancelOidcLogin(): Result { + override suspend fun cancelOAuthLogin(): Result { return withContext(coroutineDispatchers.io) { runCatchingExceptions { pendingOAuthAuthorizationData?.use { - currentClient?.abortOidcAuth(it) + currentClient?.abortOauthAuth(it) } pendingOAuthAuthorizationData = null }.mapFailure { failure -> - Timber.e(failure, "Failed to cancel OIDC login") + Timber.e(failure, "Failed to cancel OAuth login") failure.mapAuthenticationException() } } @@ -297,14 +303,14 @@ class RustMatrixAuthenticationService( } /** - * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters). + * callbackUrl should be the `url` from `OAuthAction` (with all the parameters). */ - override suspend fun loginWithOidc(callbackUrl: String): Result { + override suspend fun loginWithOAuth(callbackUrl: String): Result { return withContext(coroutineDispatchers.io) { runCatchingExceptions { val client = currentClient ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") - client.loginWithOidcCallback( + client.loginWithOauthCallback( callbackUrl = callbackUrl, ) // Free the pending data since we won't use it to abort the flow anymore @@ -316,10 +322,10 @@ class RustMatrixAuthenticationService( val sessionData = client.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, - passphrase = pendingPassphrase, + passphrase = pendingKey.formattedAsString(), sessionPaths = currentSessionPaths, ) - val matrixClient = rustMatrixClientFactory.create(client) + val matrixClient = rustMatrixClientFactory.create(client, sessionData) matrixClient.waitForKnownVerificationState() newMatrixClientObservers.forEach { it.invoke(matrixClient) } @@ -330,7 +336,7 @@ class RustMatrixAuthenticationService( SessionId(sessionData.userId) }.mapFailure { failure -> - Timber.e(failure, "Failed to login with OIDC") + Timber.e(failure, "Failed to login with OAuth") failure.mapAuthenticationException() } } @@ -355,7 +361,7 @@ class RustMatrixAuthenticationService( withContext(coroutineDispatchers.io) { val sdkQrCodeLoginData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData val emptySessionPaths = rotateSessionPath() - val oidcConfiguration = oidcConfigurationProvider.get() + val oAuthConfiguration = oAuthConfigurationProvider.get() val progressListener = object : QrLoginProgressListener { override fun onUpdate(state: QrLoginProgress) { Timber.d("QR Code login progress: $state") @@ -368,7 +374,7 @@ class RustMatrixAuthenticationService( qrCodeData = sdkQrCodeLoginData, ) client.newLoginWithQrCodeHandler( - oidcConfiguration = oidcConfiguration, + oauthConfiguration = oAuthConfiguration, ).use { it.scan( qrCodeData = qrCodeData.rustQrCodeData, @@ -381,10 +387,10 @@ class RustMatrixAuthenticationService( .toSessionData( isTokenValid = true, loginType = LoginType.QR, - passphrase = pendingPassphrase, + passphrase = pendingKey.formattedAsString(), sessionPaths = emptySessionPaths, ) - val matrixClient = rustMatrixClientFactory.create(client) + val matrixClient = rustMatrixClientFactory.create(client, sessionData) newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.addSession(sessionData) @@ -414,7 +420,7 @@ class RustMatrixAuthenticationService( return rustMatrixClientFactory .getBaseClientBuilder( sessionPaths = sessionPaths, - passphrase = pendingPassphrase, + clientSecret = pendingKey, slidingSyncType = ClientBuilderSlidingSync.Discovered, ) .config() @@ -426,13 +432,30 @@ class RustMatrixAuthenticationService( qrCodeData: QrCodeData, ): Client { Timber.d("Creating client for QR Code login with simplified sliding sync") + // The 2025 version of MSC4108 provides baseUrl; the 2024 version has null baseUrl and uses + // serverName instead, which can be null or malformed. We only enforce presence/non-blankness + // here and rely on serverNameOrHomeserverUrl()/the Rust builder layer to validate structure. + val baseUrlOrServerName = qrCodeData.baseUrl() ?: qrCodeData.serverName() + + if (baseUrlOrServerName == null) { + // With the 2024 version of MSC4108 we treat the absence of serverName as meaning that + // the other device is not signed in. + Timber.e("The QR code is from a device that is not yet signed in") + throw HumanQrLoginException.OtherDeviceNotSignedIn() + } + + if (baseUrlOrServerName.isBlank()) { + Timber.e("The QR code contains an empty base URL or server name, which is invalid") + throw HumanQrLoginException.Unknown() + } + return rustMatrixClientFactory .getBaseClientBuilder( sessionPaths = sessionPaths, - passphrase = pendingPassphrase, + clientSecret = pendingKey, slidingSyncType = ClientBuilderSlidingSync.Discovered, ) - .serverNameOrHomeserverUrl(qrCodeData.serverName()!!) + .serverNameOrHomeserverUrl(baseUrlOrServerName) .build() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt index ae56cb10fa..23b9c6be5e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapper.kt @@ -42,7 +42,7 @@ object QrErrorMapper { is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown - is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid + is RustHumanQrLoginException.OAuthMetadataInvalid -> QrLoginException.OAuthMetadataInvalid is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt deleted file mode 100644 index 39be03ce5b..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt +++ /dev/null @@ -1,76 +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.libraries.matrix.impl.certificates - -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import timber.log.Timber -import java.security.KeyStore -import java.security.KeyStoreException - -@ContributesBinding(AppScope::class) -class DefaultUserCertificatesProvider : UserCertificatesProvider { - /** - * Get additional user-installed certificates from the `AndroidCAStore` `Keystore`. - * - * The Rust HTTP client doesn't include user-installed certificates in its internal certificate - * store. This means that whatever the user installs will be ignored. - * - * While most users don't need user-installed certificates some special deployments or debugging - * setups using a proxy might want to use them. - * - * @return A list of byte arrays where each byte array is a single user-installed certificate - * in encoded form. - */ - override fun provides(): List { - // At least for API 34 the `AndroidCAStore` `Keystore` type contained user certificates as well. - // I have not found this to be documented anywhere. - val keyStore: KeyStore = try { - KeyStore.getInstance("AndroidCAStore") - } catch (e: KeyStoreException) { - Timber.w(e, "Failed to get AndroidCAStore keystore") - return emptyList() - } - val aliases = try { - keyStore.load(null) - keyStore.aliases() - } catch (e: Exception) { - Timber.w(e, "Failed to load and get aliases AndroidCAStore keystore") - return emptyList() - } - return aliases.toList() - .filter { alias -> - // The certificate alias always contains the prefix `system` or - // `user` and the MD5 subject hash separated by a colon. - // - // The subject hash can be calculated using openssl as such: - // openssl x509 -subject_hash_old -noout -in mycert.cer - // - // Again, I have not found this to be documented somewhere. - alias.startsWith("user") - } - .mapNotNull { alias -> - try { - keyStore.getEntry(alias, null) - } catch (e: Exception) { - Timber.w(e, "Failed to get entry for alias $alias") - null - } - } - .filterIsInstance() - .map { trustedCertificateEntry -> - trustedCertificateEntry.trustedCertificate.encoded - } - .also { - // Let's at least log the number of user-installed certificates we found, - // since the alias isn't particularly useful nor does the issuer seem to - // be easily available. - Timber.i("Found ${it.size} additional user-provided certificates.") - } - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index f67b7cb7e1..74f235f9f8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -131,19 +131,28 @@ class RustEncryptionService( override suspend fun enableRecovery( waitForBackupsToUpload: Boolean, - ): Result = withContext(dispatchers.io) { + passphrase: String?, + ): Result = withContext(dispatchers.io) { runCatchingExceptions { - service.enableRecovery( + // The key arrives as the suspend return value (like resetRecoveryKey), avoiding a + // flow/return-value race; the listener only feeds sub-progress. + enableRecoveryProgressStateFlow.value = EnableRecoveryProgress.Starting + val key = service.enableRecovery( waitForBackupsToUpload = waitForBackupsToUpload, progressListener = object : EnableRecoveryProgressListener { override fun onUpdate(status: RustEnableRecoveryProgress) { enableRecoveryProgressStateFlow.value = enableRecoveryProgressMapper.map(status) } }, - passphrase = null, + passphrase = passphrase, ) - // enableRecovery returns the encryption key, but we read it from the state flow - .let { } + // Pin Done explicitly so observers get a coherent terminal value. For the passphrase + // path the user never sees the SDK base58 key, so keep it out of this session-scoped + // (in-memory) flow entirely; the caller still receives it as the return value and is + // responsible for scrubbing it. The auto-generated path must retain the key here since + // that is how it is surfaced to the user. + enableRecoveryProgressStateFlow.value = EnableRecoveryProgress.Done(if (passphrase != null) "" else key) + key }.mapFailure { it.mapRecoveryException() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt index 4813ec1cc3..7ee9c7d0b3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.encryption import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityOAuthResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import org.matrix.rustcomponents.sdk.AuthData @@ -25,7 +25,7 @@ object RustIdentityResetHandleFactory { return runCatchingExceptions { identityResetHandle?.let { when (val authType = identityResetHandle.authType()) { - is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) + is CrossSigningResetAuthType.OAuth -> RustIdentityOAuthResetHandle(identityResetHandle, authType.info.approvalUrl) // User interactive authentication (user + password) CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle) } @@ -47,11 +47,11 @@ class RustPasswordIdentityResetHandle( } } -class RustOidcIdentityResetHandle( +class RustIdentityOAuthResetHandle( private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, override val url: String, -) : IdentityOidcResetHandle { - override suspend fun resetOidc(): Result { +) : IdentityOAuthResetHandle { + override suspend fun resetOAuth(): Result { return runCatchingExceptions { identityResetHandle.reset(null) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt index eefa6d4986..27d799f67b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.exception import io.element.android.libraries.matrix.api.exception.ClientException +import io.element.android.libraries.matrix.api.exception.ContentScannerErrorReason import org.matrix.rustcomponents.sdk.ClientException as RustClientException fun Throwable.mapClientException(): ClientException { @@ -23,6 +24,10 @@ fun Throwable.mapClientException(): ClientException { details = details, cause = this, ) + is RustClientException.ContentScanner -> ClientException.ContentScanner( + message = message, + reason = ContentScannerErrorReason.fromRust(reason), + ) } } else -> ClientException.Other(message ?: "Unknown error", this) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ContentScannerErrorReason.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ContentScannerErrorReason.kt new file mode 100644 index 0000000000..085a570278 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ContentScannerErrorReason.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.impl.exception + +import io.element.android.libraries.matrix.api.exception.ContentScannerErrorReason +import uniffi.matrix_sdk_contentscanner.ErrorReason +import uniffi.matrix_sdk_contentscanner.ErrorReason.MCS_BAD_DECRYPTION +import uniffi.matrix_sdk_contentscanner.ErrorReason.MCS_MALFORMED_JSON +import uniffi.matrix_sdk_contentscanner.ErrorReason.MCS_MEDIA_FAILED_TO_DECRYPT +import uniffi.matrix_sdk_contentscanner.ErrorReason.MCS_MEDIA_NOT_CLEAN +import uniffi.matrix_sdk_contentscanner.ErrorReason.MCS_MEDIA_REQUEST_FAILED +import uniffi.matrix_sdk_contentscanner.ErrorReason.MCS_MIME_TYPE_FORBIDDEN +import uniffi.matrix_sdk_contentscanner.ErrorReason.M_MISSING_TOKEN +import uniffi.matrix_sdk_contentscanner.ErrorReason.M_NOT_FOUND +import uniffi.matrix_sdk_contentscanner.ErrorReason.M_UNKNOWN +import uniffi.matrix_sdk_contentscanner.ErrorReason.M_UNKNOWN_TOKEN + +fun ContentScannerErrorReason.Companion.fromRust(reason: ErrorReason) = when (reason) { + M_UNKNOWN -> ContentScannerErrorReason.M_UNKNOWN + M_MISSING_TOKEN -> ContentScannerErrorReason.M_MISSING_TOKEN + M_UNKNOWN_TOKEN -> ContentScannerErrorReason.M_UNKNOWN_TOKEN + MCS_MALFORMED_JSON -> ContentScannerErrorReason.MCS_MALFORMED_JSON + MCS_MEDIA_FAILED_TO_DECRYPT -> ContentScannerErrorReason.MCS_MEDIA_FAILED_TO_DECRYPT + M_NOT_FOUND -> ContentScannerErrorReason.M_NOT_FOUND + MCS_MEDIA_NOT_CLEAN -> ContentScannerErrorReason.MCS_MEDIA_NOT_CLEAN + MCS_MIME_TYPE_FORBIDDEN -> ContentScannerErrorReason.MCS_MIME_TYPE_FORBIDDEN + MCS_BAD_DECRYPTION -> ContentScannerErrorReason.MCS_BAD_DECRYPTION + MCS_MEDIA_REQUEST_FAILED -> ContentScannerErrorReason.MCS_MEDIA_REQUEST_FAILED +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt deleted file mode 100644 index d13ff7779e..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.keys - -import android.util.Base64 -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import java.security.SecureRandom - -private const val SECRET_SIZE = 256 - -@ContributesBinding(AppScope::class) -class DefaultPassphraseGenerator : PassphraseGenerator { - override fun generatePassphrase(): String? { - val key = ByteArray(size = SECRET_SIZE) - SecureRandom().nextBytes(key) - return Base64.encodeToString(key, Base64.NO_PADDING or Base64.NO_WRAP) - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultSecretGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultSecretGenerator.kt new file mode 100644 index 0000000000..bad4ecb169 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultSecretGenerator.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2024, 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.keys + +import android.util.Base64 +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.androidutils.crypto.ClientSecret +import java.security.SecureRandom + +@ContributesBinding(AppScope::class) +class DefaultSecretGenerator : SecretGenerator { + override fun generatePassphrase(size: Int): ClientSecret.Passphrase? { + val key = ByteArray(size = size) + SecureRandom().nextBytes(key) + return ClientSecret.Passphrase(Base64.encodeToString(key, Base64.NO_PADDING or Base64.NO_WRAP)) + } + + override fun generateKey(size: Int): ClientSecret.RawKey { + val key = ByteArray(size = size) + SecureRandom().nextBytes(key) + return ClientSecret.RawKey(key) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt deleted file mode 100644 index e0f925fa28..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.keys - -interface PassphraseGenerator { - /** - * Generate a passphrase to encrypt the databases of a session. - * Return null to not encrypt the databases. - */ - fun generatePassphrase(): String? -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/SecretGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/SecretGenerator.kt new file mode 100644 index 0000000000..6dfade9b7c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/SecretGenerator.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2024, 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.keys + +import io.element.android.libraries.androidutils.crypto.ClientSecret + +private const val PASSPHRASE_SIZE = 256 +private const val KEY_SIZE = 32 + +interface SecretGenerator { + /** + * Generate a passphrase to encrypt the databases. + * @param size the size of the passphrase in bytes, before encoding. The default value is 256 bytes. + * @return either a random passphrase or `null` to not encrypt the databases. + */ + fun generatePassphrase(size: Int = PASSPHRASE_SIZE): ClientSecret.Passphrase? + + /** + * Generate a key to encrypt the databases. + * @param size the size of the key in bytes. The default value is 32 bytes. + * @return a random key. + */ + fun generateKey(size: Int = KEY_SIZE): ClientSecret.RawKey +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt index 211bdc3d4e..83e657eb07 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt @@ -54,6 +54,8 @@ class RustLinkDesktopHandler( } } ) + // We emit Done in case the progress listener was deallocated before scan() sent the Done + _linkDesktopStep.emit(LinkDesktopStep.Done) } catch (e: QrCodeDecodeException) { Timber.tag(tag.value).w(e, "Invalid QR code scanned") _linkDesktopStep.emit( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt index 0189987d96..a44a8d5943 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt @@ -49,6 +49,21 @@ class RustLinkMobileHandler( } } ) + // We emit Done in case the progress listener was deallocated before generate() sent the Done + _linkMobileStep.emit(LinkMobileStep.Done) + } catch (e: HumanQrGrantLoginException.Expired) { + // Note: the SDK does not return this error, but is returning `.NotFound` instead when the QR code expires. We catch both just in case. + Timber.tag(tag.value).w(e, "QR code has expired") + _linkMobileStep.emit(LinkMobileStep.QrRotating) + } catch (e: HumanQrGrantLoginException.NotFound) { + Timber.tag(tag.value).w(e, "Error during QR login grant") + // Catch timeout here? + if (_linkMobileStep.value is LinkMobileStep.QrReady) { + Timber.tag(tag.value).d("Emit QrRotating due to HumanQrGrantLoginException.NotFound") + _linkMobileStep.emit(LinkMobileStep.QrRotating) + } else { + _linkMobileStep.emit(LinkMobileStep.Error(e.map())) + } } catch (e: HumanQrGrantLoginException) { Timber.tag(tag.value).w(e, "Error during QR login grant") _linkMobileStep.emit(LinkMobileStep.Error(e.map())) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index 3199ebf71a..b5964f1a2e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.mapper import io.element.android.libraries.matrix.api.auth.external.ExternalSession -import io.element.android.libraries.matrix.impl.paths.SessionPaths +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData import org.matrix.rustcomponents.sdk.Session @@ -27,7 +27,7 @@ internal fun Session.toSessionData( accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl ?: this.homeserverUrl, - oidcData = oidcData, + oAuthData = oauthData, loginTimestamp = Date(), isTokenValid = isTokenValid, loginType = loginType, @@ -52,7 +52,7 @@ internal fun ExternalSession.toSessionData( accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl, - oidcData = null, + oAuthData = null, loginTimestamp = Date(), isTokenValid = isTokenValid, loginType = loginType, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index f1996dd942..bfb49d6ceb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -17,7 +17,6 @@ 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.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.services.toolbox.api.systemclock.SystemClock import org.matrix.rustcomponents.sdk.NotificationEvent @@ -37,10 +36,6 @@ class NotificationMapper( ): Result { return runCatchingExceptions { notificationItem.use { item -> - val isDm = isDm( - isDirect = item.roomInfo.isDirect, - activeMembersCount = item.roomInfo.joinedMembersCount.toInt(), - ) val timestamp = item.timestamp() ?: clock.epochMillis() NotificationData( sessionId = sessionId, @@ -50,10 +45,10 @@ class NotificationMapper( senderAvatarUrl = item.senderInfo.avatarUrl, senderDisplayName = item.senderInfo.displayName, senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous, - roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { isDm }, + roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { item.roomInfo.isDm }, roomDisplayName = item.roomInfo.displayName, isDirect = item.roomInfo.isDirect, - isDm = isDm, + isDm = item.roomInfo.isDm, isSpace = item.roomInfo.isSpace, isEncrypted = item.roomInfo.isEncrypted.orFalse(), isNoisy = item.isNoisy.orFalse(), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index 7da0f14d14..6a88e5051b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -62,11 +62,11 @@ class RustNotificationSettingsService( override suspend fun setDefaultRoomNotificationMode( isEncrypted: Boolean, mode: RoomNotificationMode, - isOneToOne: Boolean + isDM: Boolean ): Result = withContext(dispatchers.io) { runCatchingExceptions { try { - notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isDM, mode.let(RoomNotificationSettingsMapper::mapMode)) } catch (exception: NotificationSettingsException.RuleNotFound) { // `setDefaultRoomNotificationMode` updates multiple rules including unstable rules (e.g. the polls push rules defined in the MSC3930) // since production home servers may not have these rules yet, we drop the RuleNotFound error diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementAction.kt similarity index 86% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementAction.kt index f86c57543a..974ae08923 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementAction.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementAction.kt @@ -6,9 +6,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.oidc +package io.element.android.libraries.matrix.impl.oauth -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManagementAction fun AccountManagementAction.toRustAction(): RustAccountManagementAction { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPaths.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPaths.kt index 1888cd0f88..8a2403f772 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPaths.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPaths.kt @@ -8,19 +8,10 @@ package io.element.android.libraries.matrix.impl.paths +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.sessionstorage.api.SessionData import java.io.File -data class SessionPaths( - val fileDirectory: File, - val cacheDirectory: File, -) { - fun deleteRecursively() { - fileDirectory.deleteRecursively() - cacheDirectory.deleteRecursively() - } -} - internal fun SessionData.getSessionPaths(): SessionPaths { return SessionPaths( fileDirectory = File(sessionPath), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt index d2e519fa9e..f40c134209 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/paths/SessionPathsFactory.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.paths import dev.zacsweers.metro.Inject import io.element.android.libraries.di.BaseDirectory import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.matrix.api.paths.SessionPaths import java.io.File import java.util.UUID diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 860993d4bc..2e2a36c449 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -44,7 +44,8 @@ class RustPushersService( appDisplayName = setHttpPusherData.appDisplayName, deviceDisplayName = setHttpPusherData.deviceDisplayName, profileTag = setHttpPusherData.profileTag, - lang = setHttpPusherData.lang + lang = setHttpPusherData.lang, + append = setHttpPusherData.append, ) } .mapFailure { it.mapClientException() } 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 6507cf38c8..58cbb74720 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.room +import io.element.android.appconfig.TimelineConfig import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.extensions.mapFailure @@ -45,6 +46,7 @@ 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.liveLocationSharesFlow +import io.element.android.libraries.matrix.impl.room.location.map import io.element.android.libraries.matrix.impl.room.location.timedByExpiry import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListService @@ -72,6 +74,7 @@ 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.LiveLocationException import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate import org.matrix.rustcomponents.sdk.SendQueueListener @@ -220,7 +223,13 @@ class JoinedRustRoom( ) is CreateTimelineParams.Focused, CreateTimelineParams.PinnedOnly, - is CreateTimelineParams.Threaded -> TimelineFilter.All + is CreateTimelineParams.Threaded -> { + RustTimelineEventFilterFactory().create( + joinRule = roomInfoFlow.value.joinRule, + isEncrypted = roomInfoFlow.value.isEncrypted, + excludedStateTypes = TimelineConfig.excludedEvents, + )?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All + } } val internalIdPrefix = when (createTimelineParams) { @@ -346,7 +355,7 @@ class JoinedRustRoom( roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) runCatchingExceptions { val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow() - notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() + notificationSettingsService.getRoomNotificationSettings(roomId = roomId, isEncrypted = isEncrypted, isOneToOne = isDm()).getOrThrow() }.map { roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Ready(it) }.onFailure { @@ -419,6 +428,8 @@ class JoinedRustRoom( roomAvatar = roomPowerLevelsValues.roomAvatar, roomTopic = roomPowerLevelsValues.roomTopic, spaceChild = roomPowerLevelsValues.spaceChild, + beacon = roomPowerLevelsValues.beacon, + beaconInfo = roomPowerLevelsValues.beaconInfo, ) innerRoom.applyPowerLevelChanges(changes) } @@ -516,21 +527,37 @@ class JoinedRustRoom( return innerRoom.liveLocationSharesFlow().timedByExpiry(systemClock::epochMillis) } - override suspend fun startLiveLocationShare(durationMillis: Long): Result = withContext(roomDispatcher) { + override suspend fun startLiveLocationShare(durationMillis: Long): Result = withContext(roomDispatcher) { runCatchingExceptions { innerRoom.startLiveLocationShare(durationMillis.toULong()) - } + }.map(::EventId) } override suspend fun stopLiveLocationShare(): Result = withContext(roomDispatcher) { runCatchingExceptions { innerRoom.stopLiveLocationShare() + }.mapFailure { throwable -> + when (throwable) { + is LiveLocationException -> throwable.map() + else -> throwable + } } } override suspend fun sendLiveLocation(geoUri: String): Result = withContext(roomDispatcher) { runCatchingExceptions { innerRoom.sendLiveLocation(geoUri) + }.mapFailure { throwable -> + when (throwable) { + is LiveLocationException -> throwable.map() + else -> throwable + } + } + } + + override suspend fun setOwnMemberDisplayName(displayName: String): Result = withContext(roomDispatcher) { + runCatchingExceptions { + innerRoom.setOwnMemberDisplayName(displayName) } } @@ -538,7 +565,7 @@ class JoinedRustRoom( override fun destroy() { baseRoom.destroy() - liveInnerTimeline.destroy() + liveTimeline.close() threadsListService.destroy() Timber.d("Room $roomId destroyed") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt index e000c2f916..39cde68c31 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt @@ -49,7 +49,7 @@ class RoomContentForwarder( val content = (messageLikeContent.kind as? MsgLikeKind.Message)?.content ?: throw ForwardEventException(toRoomIds) - val targetRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } + val targetRooms = toRoomIds.toSet().mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } val failedForwardingTo = mutableSetOf() targetRooms.parallelMap { room -> room.use { targetRoom -> diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExt.kt index 668cfc46df..4c8da1aca7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExt.kt @@ -18,7 +18,7 @@ import org.matrix.rustcomponents.sdk.RoomInfo */ fun RoomInfo.elementHeroes(): List { return heroes - .takeIf { isDirect && activeMembersCount.toLong() == 2L } + .takeIf { isDm } ?.takeIf { it.size == 1 } ?.map { it.map() } .orEmpty() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt index deca0f8ee6..a972b3c130 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapper.kt @@ -43,6 +43,7 @@ class RoomInfoMapper { avatarUrl = it.avatarUrl, isPublic = it.isPublic, isDirect = it.isDirect, + isDm = it.isDm, isEncrypted = when (it.encryptionState) { EncryptionState.ENCRYPTED -> true EncryptionState.NOT_ENCRYPTED -> false @@ -76,6 +77,7 @@ class RoomInfoMapper { privilegedCreatorRole = it.privilegedCreatorsRole, isLowPriority = it.isLowPriority, activeCallIntentConsensus = it.activeRoomCallConsensusIntent.map(), + fullyReadEventId = it.fullyReadEventId?.let(::EventId) ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index e73bed084e..7c5cd10658 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -23,7 +23,6 @@ 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.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.draft.ComposerDraft -import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom @@ -119,7 +118,7 @@ class RustBaseRoom( innerRoom.membersNoSync().use { members -> members.nextChunk(members.len()) ?.map(RoomMemberMapper::map) - ?.firstOrNull { roomMember -> roomMember.userId != sessionId && roomMember.membership.isActive() } + ?.firstOrNull { roomMember -> !roomMember.isServiceMember && roomMember.userId != sessionId && roomMember.membership.isActive() } } } else { null @@ -201,12 +200,6 @@ class RustBaseRoom( } } - override suspend fun clearEventCacheStorage(): Result = withContext(roomDispatcher) { - runCatchingExceptions { - innerRoom.clearEventCacheStorage() - } - } - override suspend fun setIsFavorite(isFavorite: Boolean): Result = withContext(roomDispatcher) { runCatchingExceptions { innerRoom.setIsFavourite(isFavorite, null) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index 15a0850184..b5f9c3ee80 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.awaitLoaded +import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction @@ -41,6 +42,7 @@ import org.matrix.rustcomponents.sdk.TimelineConfiguration import org.matrix.rustcomponents.sdk.TimelineFilter import org.matrix.rustcomponents.sdk.TimelineFocus import timber.log.Timber +import uniffi.matrix_sdk_base.EncryptionState import uniffi.matrix_sdk_ui.TimelineReadReceiptTracking import java.util.concurrent.atomic.AtomicBoolean import org.matrix.rustcomponents.sdk.RoomListService as InnerRoomListService @@ -66,12 +68,6 @@ class RustRoomFactory( private val mutex = Mutex() private val isDestroyed: AtomicBoolean = AtomicBoolean(false) - private val eventFilters = TimelineConfig.excludedEvents - .takeIf { it.isNotEmpty() } - ?.let { listStateEventType -> - timelineEventFilterFactory.create(listStateEventType) - } - suspend fun destroy() { withContext(NonCancellable + dispatcher) { mutex.withLock { @@ -128,10 +124,20 @@ class RustRoomFactory( operation = "sdkRoom.timelineWithConfiguration", description = "Get timeline from the SDK", ) { + val isEncrypted = when (roomInfo.encryptionState) { + EncryptionState.ENCRYPTED -> true + EncryptionState.NOT_ENCRYPTED -> false + EncryptionState.UNKNOWN -> null + } + val timelineFilter = timelineEventFilterFactory.create( + joinRule = roomInfo.joinRule?.map(), + isEncrypted = isEncrypted, + excludedStateTypes = TimelineConfig.excludedEvents, + ) sdkRoom.timelineWithConfiguration( TimelineConfiguration( focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents), - filter = eventFilters?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All, + filter = timelineFilter?.let(TimelineFilter::EventFilter) ?: TimelineFilter.All, internalIdPrefix = "live", dateDividerMode = DateDividerMode.DAILY, trackReadReceipts = TimelineReadReceiptTracking.ALL_EVENTS, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt index b4735fdaf3..a966acaa2b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/TimelineEventFilterFactory.kt @@ -11,20 +11,45 @@ package io.element.android.libraries.matrix.impl.room import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.join.JoinRule +import org.matrix.rustcomponents.sdk.FilterTimelineEventCondition import org.matrix.rustcomponents.sdk.FilterTimelineEventType import org.matrix.rustcomponents.sdk.TimelineEventFilter +import uniffi.matrix_sdk_ui.MembershipChangeFilter interface TimelineEventFilterFactory { - fun create(listStateEventType: List): TimelineEventFilter + fun create( + joinRule: JoinRule?, + isEncrypted: Boolean?, + excludedStateTypes: List + ): TimelineEventFilter? } @ContributesBinding(AppScope::class) class RustTimelineEventFilterFactory : TimelineEventFilterFactory { - override fun create(listStateEventType: List): TimelineEventFilter { - return TimelineEventFilter.excludeEventTypes( - listStateEventType.map { stateEventType -> - FilterTimelineEventType.State(stateEventType.map()) - } - ) + override fun create( + joinRule: JoinRule?, + isEncrypted: Boolean?, + excludedStateTypes: List + ): TimelineEventFilter? { + val excludedEventTypes = excludedStateTypes.map { + FilterTimelineEventCondition.EventType(FilterTimelineEventType.State(it.map())) + } + // If the room is publicly joinable and not encrypted, we also want to exclude membership changes and profile changes, + // as they will pollute the timelines since they're quite common and not add much value. + val excludedMembershipChanges = if (joinRule !is JoinRule.Invite && isEncrypted == false) { + listOf( + FilterTimelineEventCondition.MembershipChange(MembershipChangeFilter.JOIN), + FilterTimelineEventCondition.MembershipChange(MembershipChangeFilter.LEAVE), + FilterTimelineEventCondition.ProfileChange, + ) + } else { + emptyList() + } + return if (excludedEventTypes.isNotEmpty() || excludedMembershipChanges.isNotEmpty()) { + TimelineEventFilter.exclude(excludedEventTypes + excludedMembershipChanges) + } else { + null + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/BeaconInfoUpdates.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/BeaconInfoUpdates.kt new file mode 100644 index 0000000000..44be305c02 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/BeaconInfoUpdates.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.matrix.impl.room.location + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate +import org.matrix.rustcomponents.sdk.BeaconInfoUpdate as RustBeaconInfoUpdate + +fun RustBeaconInfoUpdate.map(): BeaconInfoUpdate { + return BeaconInfoUpdate( + roomId = RoomId(roomId), + beaconId = EventId(eventId), + isLive = live + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationException.kt new file mode 100644 index 0000000000..b10f5cd41c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationException.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.impl.room.location + +import io.element.android.libraries.matrix.api.room.location.LiveLocationException +import org.matrix.rustcomponents.sdk.LiveLocationException as RustLiveLocationException + +fun RustLiveLocationException.map(): LiveLocationException { + return when (this) { + is RustLiveLocationException.Network -> LiveLocationException.Network() + is RustLiveLocationException.NotLive -> LiveLocationException.NotLive() + else -> LiveLocationException.Other(this) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationSharesFlow.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationSharesFlow.kt index bae406a137..8e8181539f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationSharesFlow.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/location/LiveLocationSharesFlow.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.room.location +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.location.LastLocation import io.element.android.libraries.matrix.api.room.location.LiveLocationShare @@ -16,8 +17,8 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.callbackFlow -import org.matrix.rustcomponents.sdk.LiveLocationShareListener import org.matrix.rustcomponents.sdk.LiveLocationShareUpdate +import org.matrix.rustcomponents.sdk.LiveLocationsListener import org.matrix.rustcomponents.sdk.RoomInterface import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare @@ -41,9 +42,9 @@ fun RoomInterface.liveLocationSharesFlow(): Flow> { } } return callbackFlow { - val liveLocationShares = liveLocationShares() + val observer = liveLocationsObserver() val shares: MutableList = ArrayList() - val taskHandle = liveLocationShares.subscribe(object : LiveLocationShareListener { + val taskHandle = observer.subscribe(object : LiveLocationsListener { override fun onUpdate(updates: List) { for (update in updates) { shares.applyUpdate(update) @@ -53,13 +54,14 @@ fun RoomInterface.liveLocationSharesFlow(): Flow> { }) awaitClose { taskHandle.cancelAndDestroy() - liveLocationShares.destroy() + observer.destroy() } }.buffer(Channel.UNLIMITED) } private fun RustLiveLocationShare.into(): LiveLocationShare { return LiveLocationShare( + beaconId = EventId(beaconId), userId = UserId(userId), lastLocation = lastLocation?.let { LastLocation( @@ -69,6 +71,6 @@ private fun RustLiveLocationShare.into(): LiveLocationShare { ) }, startTimestamp = startTs.toLong(), - endTimestamp = (startTs + timeout).toLong() + endTimestamp = (startTs + timeout).toLong(), ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt index 447fa427a6..33ecb74ff3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt @@ -28,7 +28,8 @@ object RoomMemberMapper { powerLevel = powerLevel, isIgnored = roomMember.isIgnored, role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel), - membershipChangeReason = roomMember.membershipChangeReason + membershipChangeReason = roomMember.membershipChangeReason, + isServiceMember = roomMember.isServiceMember, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapper.kt index 5e2a1c82da..499868795a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapper.kt @@ -26,6 +26,8 @@ object RoomPowerLevelsValuesMapper { roomAvatar = values.roomAvatar, roomTopic = values.roomTopic, spaceChild = values.spaceChild, + beacon = values.beacon, + beaconInfo = values.beaconInfo, ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt index 27b19ebf19..5d8367bb99 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListEntriesUpdateExt.kt @@ -16,25 +16,25 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate internal fun RoomListEntriesUpdate.describe(): String { return when (this) { is RoomListEntriesUpdate.Set -> { - "Set #$index to '${value.displayName()}'" + "Set #$index to '${value.id()}'" } is RoomListEntriesUpdate.Append -> { - "Append ${values.map { "'" + it.displayName() + "'" }}" + "Append ${values.map { "'" + it.id() + "'" }}" } is RoomListEntriesUpdate.PushBack -> { - "PushBack '${value.displayName()}'" + "PushBack '${value.id()}'" } is RoomListEntriesUpdate.PushFront -> { - "PushFront '${value.displayName()}'" + "PushFront '${value.id()}'" } is RoomListEntriesUpdate.Insert -> { - "Insert at #$index: '${value.displayName()}'" + "Insert at #$index: '${value.id()}'" } is RoomListEntriesUpdate.Remove -> { "Remove #$index" } is RoomListEntriesUpdate.Reset -> { - "Reset all to ${values.map { "'" + it.displayName() + "'" }}" + "Reset all to ${values.map { "'" + it.id() + "'" }}" } RoomListEntriesUpdate.PopBack -> { "PopBack" diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index 968a768fa2..afdac3db12 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -112,6 +112,11 @@ class RoomSummaryListProcessor( private suspend fun updateRoomSummaries(updates: List, block: suspend MutableList.() -> Unit) = withContext( coroutineContext ) { + // Capture the description before applying updates: applyUpdate consumes each Room via + // `entry.use { ... }` which destroys it, and the duplicate-detection branch below reads + // id() through `describe()`. Without this capture the trackError call crashes before it + // can be reported. + val updatesDescription = updates.description() mutex.withLock { val current = roomSummaries.replayCache.lastOrNull() val mutableRoomSummaries = current.orEmpty().toMutableList() @@ -126,7 +131,7 @@ class RoomSummaryListProcessor( analyticsService.trackError( IllegalStateException( "Found duplicates in room summaries after a list update from the SDK: $duplicates. " + - "Updates: ${updates.description()}" + "Updates: $updatesDescription" ) ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt index f83cd648a6..cd729d0df1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt @@ -36,6 +36,7 @@ class SpaceRoomMapper { worldReadable = spaceRoom.worldReadable.orFalse(), via = spaceRoom.via.toImmutableList(), isDirect = spaceRoom.isDirect, + isDm = spaceRoom.isDm, ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilder.kt index 84f124868e..4cfcf1a95e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilder.kt @@ -7,14 +7,25 @@ package io.element.android.libraries.matrix.impl.storage +import io.element.android.libraries.androidutils.crypto.ClientSecret import io.element.android.libraries.core.data.ByteUnit import io.element.android.libraries.core.data.megaBytes -import io.element.android.libraries.matrix.impl.paths.SessionPaths +import io.element.android.libraries.matrix.api.paths.SessionPaths import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder +/** + * Abstraction over the SDK's [SdkSqliteStoreBuilder] to allow configuring it with a ClientSecret and to hide the SDK from the rest of the codebase. + */ interface SqliteStoreBuilder { - fun passphrase(passphrase: String?): SqliteStoreBuilder + /** + * Configure the builder with a [ClientSecret], if provided. If the [clientSecret] is null, the databases will not be encrypted. + */ + fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder + + /** + * Configure the provided [clientBuilder] with the configured [SdkSqliteStoreBuilder] and return it. + */ fun setupClientBuilder(clientBuilder: ClientBuilder): ClientBuilder } @@ -26,8 +37,15 @@ class RustSqliteStoreBuilder( cachePath = sessionPaths.cacheDirectory.absolutePath, ).journalSizeLimit(25.megaBytes.into(ByteUnit.BYTES).toUInt()) - override fun passphrase(passphrase: String?): SqliteStoreBuilder { - inner = inner.passphrase(passphrase) + override fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder { + when (clientSecret) { + null -> Unit + is ClientSecret.Passphrase -> inner = inner.passphrase(clientSecret.value) + is ClientSecret.RawKey -> { + // Ensure the key is 32 bytes long, as required by the SDK + inner = inner.key(clientSecret.keyOfSize(32)) + } + } return this } @@ -35,3 +53,15 @@ class RustSqliteStoreBuilder( return clientBuilder.sqliteStore(this.inner) } } + +private fun ClientSecret.RawKey.keyOfSize(size: Int): ByteArray { + return if (bytes.size == 32) { + bytes + } else if (bytes.size < 32) { + // If the key is shorter than 32 bytes, pad it with zeros + bytes + ByteArray(32 - bytes.size) + } else { + // Otherwise, take the first 32 bytes of the key + bytes.copyOfRange(0, 32) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilderProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilderProvider.kt index 29a30f3c56..5d79e72403 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilderProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/storage/SqliteStoreBuilderProvider.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.storage import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.matrix.impl.paths.SessionPaths +import io.element.android.libraries.matrix.api.paths.SessionPaths interface SqliteStoreBuilderProvider { fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder 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 7b398529a0..36a8521fb7 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 @@ -12,6 +12,7 @@ import io.element.android.libraries.androidutils.hash.hash 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.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -20,7 +21,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo 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.JoinedRoom -import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.join.JoinRule 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 @@ -44,6 +45,7 @@ import io.element.android.libraries.matrix.impl.timeline.postprocessor.TypingNot import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper import io.element.android.libraries.matrix.impl.util.MessageEventContent import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -82,7 +84,7 @@ private const val PAGINATION_SIZE = 50 class RustTimeline( private val inner: InnerTimeline, override val mode: Timeline.Mode, - private val systemClock: SystemClock, + systemClock: SystemClock, private val joinedRoom: JoinedRoom, private val coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, @@ -122,6 +124,13 @@ class RustTimeline( private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode) + private data class RoomTimelineInfo( + val roomCreators: ImmutableList, + val isDm: Boolean, + val joinRule: JoinRule?, + val isEncrypted: Boolean?, + ) + override val backwardPaginationStatus = MutableStateFlow( Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PinnedEvents) ) @@ -221,14 +230,15 @@ class RustTimeline( _timelineItems, backwardPaginationStatus, forwardPaginationStatus, - joinedRoom.roomInfoFlow.map { it.creators to it.isDm }.distinctUntilChanged(), + joinedRoom.roomInfoFlow.map { RoomTimelineInfo(it.creators, it.isDm, it.joinRule, it.isEncrypted) }.distinctUntilChanged(), ) { timelineItems, backwardPaginationStatus, forwardPaginationStatus, - (roomCreators, isDm), + roomInfo, -> withContext(dispatcher) { + val (roomCreators, isDm, joinRule, isEncrypted) = roomInfo timelineItems .let { items -> roomBeginningPostProcessor.process( 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 edfe9a3543..fa671bc546 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 @@ -153,7 +153,8 @@ class TimelineEventContentMapper( CallIntent.AUDIO } else { CallIntent.VIDEO - } + }, + declinedBy = it.declinedBy.map(::UserId) ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index 397280231d..8991d26f9c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent /** * This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs - * or add the RoomBeginning item. + * or add the RoomBeginning item. For rooms that aren't invite-only and aren't encrypted, it also removes join/leave and profile change events. */ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { fun process( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProvider.kt deleted file mode 100644 index 0030c6e23e..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.util - -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.impl.paths.SessionPaths -import io.element.android.libraries.matrix.impl.paths.getSessionPaths -import io.element.android.libraries.sessionstorage.api.SessionStore - -class SessionPathsProvider( - private val sessionStore: SessionStore, -) { - suspend fun provides(sessionId: SessionId): SessionPaths? { - val sessionData = sessionStore.getSession(sessionId.value) ?: return null - return sessionData.getSessionPaths() - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Token.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Token.kt index 815e134cf2..f5df21008b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Token.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Token.kt @@ -16,7 +16,8 @@ private val sha256 by lazy { MessageDigest.getInstance("SHA-256") } @OptIn(ExperimentalStdlibApi::class) private fun anonymizeToken(token: String): String { - return sha256.digest(token.toByteArray()).toHexString() + // Only keep the first 32 chars (16 bytes) of the hashed token to avoid displaying too much information. + return sha256.digest(token.toByteArray()).toHexString().take(32) } fun SessionData?.anonymizedTokens(): Pair { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 7fb2935897..b2a13f5f1a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -124,7 +124,7 @@ class RustSessionVerificationService( this.listener = listener } - override suspend fun requestCurrentSessionVerification() = tryOrFail { + override suspend fun requestDeviceVerification() = tryOrFail { ensureEncryptionIsInitialized() verificationController.requestDeviceVerification() currentVerificationRequest = VerificationRequest.Outgoing.CurrentSession @@ -146,7 +146,7 @@ class RustSessionVerificationService( override suspend fun declineVerification() = tryOrFail { verificationController.declineVerification() } - override suspend fun startVerification() = tryOrFail { + override suspend fun startSasVerification() = tryOrFail { verificationController.startSasVerification() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt index 6aa3ef0e5b..0036f2f962 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustClientSessionDelegateTest.kt @@ -16,15 +16,11 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import uniffi.matrix_sdk_common.BackgroundTaskFailureReason -@OptIn(ExperimentalCoroutinesApi::class) class RustClientSessionDelegateTest { @Test fun `saveSessionInKeychain should update the store`() = runTest { @@ -43,7 +39,6 @@ class RustClientSessionDelegateTest { refreshToken = "rt", ) ) - runCurrent() val result = sessionStore.getLatestSession() assertThat(result!!.accessToken).isEqualTo("at") assertThat(result.refreshToken).isEqualTo("rt") @@ -80,5 +75,4 @@ fun TestScope.aRustClientSessionDelegate( sessionStore = sessionStore, appCoroutineScope = this, analyticsService = analyticsService, - coroutineDispatchers = testCoroutineDispatchers(), ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index 670430e23e..1c25acf088 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -12,7 +12,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider -import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider import io.element.android.libraries.network.useragent.SimpleUserAgentProvider @@ -58,7 +57,6 @@ fun TestScope.createRustMatrixClientFactory( coroutineDispatchers = testCoroutineDispatchers(), sessionStore = sessionStore, userAgentProvider = SimpleUserAgentProvider(), - userCertificatesProvider = FakeUserCertificatesProvider(), proxyProvider = FakeProxyProvider(), clock = FakeSystemClock(), analyticsService = FakeAnalyticsService(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index ed8440fe53..a8e812d031 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -13,6 +13,7 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.data.bytes import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory @@ -141,6 +142,7 @@ class RustMatrixClientTest { ), ) = RustMatrixClient( innerClient = client, + sessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")), sessionStore = sessionStore, appCoroutineScope = backgroundScope, sessionDelegate = aRustClientSessionDelegate( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedExtKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedExtKtTest.kt index 68adfe00a2..3c094f3581 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedExtKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedExtKtTest.kt @@ -43,6 +43,19 @@ class JoinedExtKtTest { @Test fun `test isDirect parameter mapping`() = runTest { assertThat(aRoom(isDirect = true).toAnalyticsJoinedRoom(null)) + .isEqualTo( + JoinedRoom( + isDM = false, + isSpace = false, + roomSize = JoinedRoom.RoomSize.One, + trigger = null + ) + ) + } + + @Test + fun `test isDm parameter mapping`() = runTest { + assertThat(aRoom(isDm = true).toAnalyticsJoinedRoom(null)) .isEqualTo( JoinedRoom( isDM = true, @@ -80,12 +93,13 @@ class JoinedExtKtTest { } private fun aRoom( + isDm: Boolean = false, isDirect: Boolean = false, isSpace: Boolean = false, joinedMemberCount: Long = 0 ): FakeBaseRoom { return FakeBaseRoom().apply { - givenRoomInfo(aRoomInfo(isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount)) + givenRoomInfo(aRoomInfo(isDm = isDm, isDirect = isDirect, isSpace = isSpace, joinedMembersCount = joinedMemberCount)) } } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt index 8449d9a6c3..cc5fb4a394 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt @@ -13,7 +13,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.junit.Test import org.matrix.rustcomponents.sdk.ClientBuildException -import org.matrix.rustcomponents.sdk.OidcException +import org.matrix.rustcomponents.sdk.OAuthException class AuthenticationExceptionMappingTest { @Test @@ -64,17 +64,17 @@ class AuthenticationExceptionMappingTest { } @Test - fun `mapping Oidc exceptions map to the Oidc Kotlin`() { - assertThat(OidcException.Generic("Generic").mapAuthenticationException()) - .isException("Generic") - assertThat(OidcException.CallbackUrlInvalid("CallbackUrlInvalid").mapAuthenticationException()) - .isException("CallbackUrlInvalid") - assertThat(OidcException.Cancelled("Cancelled").mapAuthenticationException()) - .isException("Cancelled") - assertThat(OidcException.MetadataInvalid("MetadataInvalid").mapAuthenticationException()) - .isException("MetadataInvalid") - assertThat(OidcException.NotSupported("NotSupported").mapAuthenticationException()) - .isException("NotSupported") + fun `mapping Oidc exceptions map to the OAuth Kotlin`() { + assertThat(OAuthException.Generic("Generic").mapAuthenticationException()) + .isException("Generic") + assertThat(OAuthException.CallbackUrlInvalid("CallbackUrlInvalid").mapAuthenticationException()) + .isException("CallbackUrlInvalid") + assertThat(OAuthException.Cancelled("Cancelled").mapAuthenticationException()) + .isException("Cancelled") + assertThat(OAuthException.MetadataInvalid("MetadataInvalid").mapAuthenticationException()) + .isException("MetadataInvalid") + assertThat(OAuthException.NotSupported("NotSupported").mapAuthenticationException()) + .isException("NotSupported") } private inline fun ThrowableSubject.isException(message: String) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakePassphraseGenerator.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakePassphraseGenerator.kt deleted file mode 100644 index a089920422..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakePassphraseGenerator.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.auth - -import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator -import io.element.android.libraries.matrix.test.A_PASSPHRASE - -class FakePassphraseGenerator( - private val passphrase: () -> String? = { A_PASSPHRASE } -) : PassphraseGenerator { - override fun generatePassphrase(): String? = passphrase() -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeSecretGenerator.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeSecretGenerator.kt new file mode 100644 index 0000000000..12c227946a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeSecretGenerator.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2024, 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth + +import io.element.android.libraries.androidutils.crypto.ClientSecret +import io.element.android.libraries.matrix.impl.keys.SecretGenerator +import io.element.android.libraries.matrix.test.A_PASSPHRASE + +class FakeSecretGenerator( + private val passphrase: (Int) -> String? = { A_PASSPHRASE }, + private val key: (Int) -> ByteArray = { ByteArray(it) { 0 } }, +) : SecretGenerator { + override fun generatePassphrase(size: Int): ClientSecret.Passphrase? = passphrase(size)?.let { ClientSecret.Passphrase(it) } + override fun generateKey(size: Int): ClientSecret.RawKey = ClientSecret.RawKey(key(size)) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeUserCertificatesProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeUserCertificatesProvider.kt deleted file mode 100644 index 955af4a23d..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeUserCertificatesProvider.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.libraries.matrix.impl.auth - -import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider - -class FakeUserCertificatesProvider : UserCertificatesProvider { - override fun provides(): List { - return emptyList() - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt index a5c7b2dbc8..63f573536c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt @@ -20,7 +20,7 @@ class HomeserverDetailsKtTest { val homeserverLoginDetails = FakeFfiHomeserverLoginDetails( url = "https://example.org", supportsPasswordLogin = true, - supportsOidcLogin = false + supportsOAuthLogin = false ) // When @@ -31,7 +31,7 @@ class HomeserverDetailsKtTest { MatrixHomeServerDetails( url = "https://example.org", supportsPasswordLogin = true, - supportsOidcLogin = false + supportsOAuthLogin = false ) ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProviderTest.kt similarity index 76% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProviderTest.kt index 095cf54946..3b54f2d1f3 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfigurationProviderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/OAuthConfigurationProviderTest.kt @@ -10,18 +10,18 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.auth.FAKE_REDIRECT_URL -import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider +import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider import io.element.android.libraries.matrix.test.core.aBuildMeta import org.junit.Test -class OidcConfigurationProviderTest { +class OAuthConfigurationProviderTest { @Test fun get() { - val result = OidcConfigurationProvider( + val result = OAuthConfigurationProvider( buildMeta = aBuildMeta( applicationName = "myName", ), - oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), + oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), ).get() assertThat(result.clientName).isEqualTo("myName") assertThat(result.redirectUri).isEqualTo(FAKE_REDIRECT_URL) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt index 50d1f3723b..862019091b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt @@ -18,8 +18,8 @@ import org.junit.Test class RustHomeserverLoginCompatibilityCheckerTest { @Test - fun `check - is valid if it supports OIDC login`() = runTest { - val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOidcLogin = true) } + fun `check - is valid if it supports OAuth login`() = runTest { + val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOAuthLogin = true) } assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue() } @@ -49,6 +49,5 @@ class RustHomeserverLoginCompatibilityCheckerTest { FakeFfiClient(homeserverLoginDetailsResult = result) } }, - userCertificatesProvider = FakeUserCertificatesProvider(), ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index f4ce7b1fdd..2b9080195f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.impl.auth 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.libraries.matrix.impl.ClientBuilderProvider import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider import io.element.android.libraries.matrix.impl.createRustMatrixClientFactory @@ -16,7 +18,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory -import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider +import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore @@ -50,6 +52,7 @@ class RustMatrixAuthenticationServiceTest { private fun TestScope.createRustMatrixAuthenticationService( sessionStore: SessionStore = InMemorySessionStore(), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ): RustMatrixAuthenticationService { val baseDirectory = File("/base") val cacheDirectory = File("/cache") @@ -63,11 +66,12 @@ class RustMatrixAuthenticationServiceTest { coroutineDispatchers = testCoroutineDispatchers(), sessionStore = sessionStore, rustMatrixClientFactory = rustMatrixClientFactory, - passphraseGenerator = FakePassphraseGenerator(), - oidcConfigurationProvider = OidcConfigurationProvider( + secretGenerator = FakeSecretGenerator(), + oAuthConfigurationProvider = OAuthConfigurationProvider( buildMeta = aBuildMeta(), - oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), + oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), ), + enterpriseService = enterpriseService, ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt index 0ef20c82a6..4de5e55985 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/QrErrorMapperTest.kt @@ -32,7 +32,7 @@ class QrErrorMapperTest { assertThat(QrErrorMapper.map(RustHumanQrLoginException.OtherDeviceNotSignedIn())).isEqualTo(QrLoginException.OtherDeviceNotSignedIn) assertThat(QrErrorMapper.map(RustHumanQrLoginException.LinkingNotSupported())).isEqualTo(QrLoginException.LinkingNotSupported) assertThat(QrErrorMapper.map(RustHumanQrLoginException.Unknown())).isEqualTo(QrLoginException.Unknown) - assertThat(QrErrorMapper.map(RustHumanQrLoginException.OidcMetadataInvalid())).isEqualTo(QrLoginException.OidcMetadataInvalid) + assertThat(QrErrorMapper.map(RustHumanQrLoginException.OAuthMetadataInvalid())).isEqualTo(QrLoginException.OAuthMetadataInvalid) assertThat(QrErrorMapper.map(RustHumanQrLoginException.SlidingSyncNotAvailable())).isEqualTo(QrLoginException.SlidingSyncNotAvailable) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt index 4db2db7107..63ad77238b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEvent import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_NAME @@ -66,8 +67,11 @@ internal fun aRustNotificationRoomInfo( joinedMembersCount: ULong = 2u, isEncrypted: Boolean? = true, isDirect: Boolean = false, + isDm: Boolean = false, joinRule: JoinRule? = null, isSpace: Boolean = false, + serviceMembers: List = emptyList(), + activeServiceMemberCount: Int = 0, ) = NotificationRoomInfo( displayName = displayName, avatarUrl = avatarUrl, @@ -76,8 +80,11 @@ internal fun aRustNotificationRoomInfo( joinedMembersCount = joinedMembersCount, isEncrypted = isEncrypted, isDirect = isDirect, + isDm = isDm, joinRule = joinRule, isSpace = isSpace, + serviceMembers = serviceMembers.map { it.value }, + activeServiceMembersCount = activeServiceMemberCount.toULong(), ) internal fun aRustNotificationEventTimeline( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt index 1b0cc12461..cb5e221dc9 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomInfo.kt @@ -62,6 +62,9 @@ internal fun aRustRoomInfo( serviceMembers: List = emptyList(), isLowPriority: Boolean = false, activeRoomCallConsensusIntent: RtcCallIntentConsensus = RtcCallIntentConsensus.None, + activeServiceMembersCount: Int = 0, + isDm: Boolean = false, + fullyReadEventId: String? = null, ) = RoomInfo( id = id, displayName = displayName, @@ -101,4 +104,7 @@ internal fun aRustRoomInfo( serviceMembers = serviceMembers, isLowPriority = isLowPriority, activeRoomCallConsensusIntent = activeRoomCallConsensusIntent, + activeServiceMembersCount = activeServiceMembersCount.toULong(), + isDm = isDm, + fullyReadEventId = fullyReadEventId, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt index 77fa814cca..6b4f958d84 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomMember.kt @@ -24,6 +24,7 @@ internal fun aRustRoomMember( isIgnored: Boolean = false, role: RoomMemberRole = RoomMemberRole.USER, membershipChangeReason: String? = null, + isServiceMember: Boolean = false, ) = RoomMember( userId = userId.value, displayName = displayName, @@ -34,4 +35,5 @@ internal fun aRustRoomMember( isIgnored = isIgnored, suggestedRoleForPowerLevel = role, membershipChangeReason = membershipChangeReason, + isServiceMember = isServiceMember, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevelsValues.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevelsValues.kt index 1c1bbb42e3..28af0093f6 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevelsValues.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/RoomPowerLevelsValues.kt @@ -22,6 +22,8 @@ internal fun aRustRoomPowerLevelsValues( roomAvatar: Long, roomTopic: Long, spaceChild: Long, + beacon: Long, + beaconInfo: Long, ) = RoomPowerLevelsValues( ban = ban, invite = invite, @@ -33,5 +35,7 @@ internal fun aRustRoomPowerLevelsValues( roomName = roomName, roomAvatar = roomAvatar, roomTopic = roomTopic, - spaceChild = spaceChild + spaceChild = spaceChild, + beacon = beacon, + beaconInfo = beaconInfo, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt index 4671c457b0..af7f44597c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/Session.kt @@ -24,6 +24,6 @@ internal fun aRustSession( userId = A_USER_ID.value, deviceId = A_DEVICE_ID.value, homeserverUrl = A_HOMESERVER_URL, - oidcData = null, + oauthData = null, slidingSyncVersion = proxy, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt index 50115055c2..2eb46eddaf 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/SpaceRoom.kt @@ -19,6 +19,7 @@ import org.matrix.rustcomponents.sdk.SpaceRoom internal fun aRustSpaceRoom( roomId: RoomId = A_ROOM_ID, isDirect: Boolean = false, + isDm: Boolean = false, canonicalAlias: String? = null, rawName: String? = null, displayName: String = "", @@ -35,6 +36,7 @@ internal fun aRustSpaceRoom( ) = SpaceRoom( roomId = roomId.value, isDirect = isDirect, + isDm = isDm, canonicalAlias = canonicalAlias, rawName = rawName, displayName = displayName, @@ -48,5 +50,5 @@ internal fun aRustSpaceRoom( childrenCount = childrenCount, state = state, heroes = heroes, - via = emptyList() + via = emptyList(), ) 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 57ffcddb37..43c36f7bfb 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 @@ -73,6 +73,7 @@ class FakeFfiClient( deviceDisplayName: String, profileTag: String?, lang: String, + append: Boolean, ) = Unit override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt index 59d82487b9..623fe9a8cf 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt @@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.RequestConfig import org.matrix.rustcomponents.sdk.SlidingSyncVersionBuilder import org.matrix.rustcomponents.sdk.SqliteStoreBuilder import uniffi.matrix_sdk.BackupDownloadStrategy +import uniffi.matrix_sdk_base.DmRoomDefinition import uniffi.matrix_sdk_crypto.CollectStrategy import uniffi.matrix_sdk_crypto.DecryptionSettings @@ -47,5 +48,6 @@ class FakeFfiClientBuilder( override fun sqliteStore(config: SqliteStoreBuilder): ClientBuilder = this override fun inMemoryStore(): ClientBuilder = this override fun crossProcessLockConfig(crossProcessLockConfig: CrossProcessLockConfig): ClientBuilder = this + override fun dmRoomDefinition(dmRoomDefinition: DmRoomDefinition): ClientBuilder = this override suspend fun build() = buildResult() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt index cd0733695b..0899b16325 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt @@ -16,8 +16,8 @@ import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.QrCodeData class FakeFfiGrantLoginWithQrCodeHandler( - private val generateResult: () -> Unit = {}, - private val scanResult: (QrCodeData) -> Unit = {}, + private val generateResult: suspend () -> Unit = {}, + private val scanResult: suspend (QrCodeData) -> Unit = {}, ) : GrantLoginWithQrCodeHandler(NoHandle) { private var generateProgressListener: GrantGeneratedQrLoginProgressListener? = null private var scanProgressListener: GrantQrLoginProgressListener? = null diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt index ade3a2328f..5351fbc8df 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt @@ -14,11 +14,11 @@ import org.matrix.rustcomponents.sdk.NoHandle class FakeFfiHomeserverLoginDetails( private val url: String = "https://example.org", private val supportsPasswordLogin: Boolean = false, - private val supportsOidcLogin: Boolean = false, + private val supportsOAuthLogin: Boolean = false, private val supportsSsoLogin: Boolean = false, ) : HomeserverLoginDetails(NoHandle) { override fun url(): String = url - override fun supportsOidcLogin(): Boolean = supportsOidcLogin + override fun supportsOauthLogin(): Boolean = supportsOAuthLogin override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin override fun supportsSsoLogin(): Boolean = supportsSsoLogin } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomPowerLevels.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomPowerLevels.kt index c47c4406b6..985dc935ff 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomPowerLevels.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomPowerLevels.kt @@ -32,4 +32,6 @@ fun defaultFfiRoomPowerLevelValues() = RoomPowerLevelsValues( roomTopic = 50, spaceChild = 50, usersDefault = 0, + beacon = 0, + beaconInfo = 0, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.kt index c0ecc53d4f..56cc5b9a1f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSpaceRoomList.kt @@ -40,7 +40,7 @@ class FakeFfiSpaceRoomList( return paginationStateResult() } - override fun rooms(): List { + override suspend fun rooms(): List { return roomsResult() } @@ -53,7 +53,7 @@ class FakeFfiSpaceRoomList( spaceRoomListPaginationStateListener?.onUpdate(state) } - override fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle { + override suspend fun subscribeToRoomUpdate(listener: SpaceRoomListEntriesListener): TaskHandle { spaceRoomListEntriesListener = listener return FakeFfiTaskHandle() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGeneratorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGeneratorTest.kt deleted file mode 100644 index 465d920074..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGeneratorTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.keys - -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class DefaultPassphraseGeneratorTest { - @Test - fun `check that generated passphrase has the expected length`() { - val passphraseGenerator = DefaultPassphraseGenerator() - val passphrase = passphraseGenerator.generatePassphrase() - assertThat(passphrase!!.length).isEqualTo(342) - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultSecretGeneratorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultSecretGeneratorTest.kt new file mode 100644 index 0000000000..b18933fb53 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultSecretGeneratorTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2024, 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.keys + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.crypto.ClientSecret +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test + +class DefaultSecretGeneratorTest : RobolectricTest() { + @Test + fun `check that generated passphrase has the expected length`() { + val secretGenerator = DefaultSecretGenerator() + val passphrase = secretGenerator.generatePassphrase(256) + assertThat(passphrase).isInstanceOf(ClientSecret.Passphrase::class.java) + // Size after Base64 encoding should be 4/3 of the original size, without padding + assertThat(passphrase!!.value).hasLength(342) + } + + @Test + fun `check that generated key has the expected length`() { + val secretGenerator = DefaultSecretGenerator() + val key = secretGenerator.generateKey(123) + assertThat(key).isInstanceOf(ClientSecret.RawKey::class.java) + assertThat(key.bytes).hasLength(123) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt index a180e4d515..fd635e2da6 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.linknewdevice.ErrorType import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler import io.element.android.libraries.matrix.test.QR_CODE_DATA +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -29,7 +30,13 @@ import org.matrix.rustcomponents.sdk.QrCodeDecodeException class RustLinkDesktopHandlerTest { @Test fun `handleScannedQrCode function works as expected`() = runTest { - val handler = FakeFfiGrantLoginWithQrCodeHandler() + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + scanResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) val sut = createRustLinkDesktopHandler( handler, ) @@ -53,6 +60,36 @@ class RustLinkDesktopHandlerTest { handler.emitScanProgress(progress) assertThat(awaitItem()).isEqualTo(expectedStep) } + // scan returns, no new event is emitted + completable.complete(Unit) + expectNoEvents() + } + } + + @Test + fun `when scan does not emits the Done state, the code emits it`() = runTest { + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + scanResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) + val sut = createRustLinkDesktopHandler( + handler, + ) + sut.linkDesktopStep.test { + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo(LinkDesktopStep.Uninitialized) + backgroundScope.launch { + sut.handleScannedQrCode(QR_CODE_DATA) + } + runCurrent() + handler.emitScanProgress(GrantQrLoginProgress.Starting) + assertThat(awaitItem()).isEqualTo(LinkDesktopStep.Starting) + // scan returns, Done event is emitted + completable.complete(Unit) + assertThat(awaitItem()).isEqualTo(LinkDesktopStep.Done) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt index aa13996e8a..3d84e34f45 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiCheckCodeS import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -30,7 +31,13 @@ import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException class RustLinkMobileHandlerTest { @Test fun `start function works as expected`() = runTest { - val handler = FakeFfiGrantLoginWithQrCodeHandler() + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) val sut = createRustLinkMobileHandler( handler, ) @@ -56,6 +63,36 @@ class RustLinkMobileHandlerTest { handler.emitGenerateProgress(progress) assertThat(awaitItem()).isInstanceOf(expectedStepClass) } + // generate returns, no new event is emitted + completable.complete(Unit) + expectNoEvents() + } + } + + @Test + fun `when generates does not emits the Done state, the code emits it`() = runTest { + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { + // Ensure that the coroutine is hold + completable.await() + } + ) + val sut = createRustLinkMobileHandler( + handler, + ) + sut.linkMobileStep.test { + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized) + backgroundScope.launch { + sut.start() + } + runCurrent() + handler.emitGenerateProgress(GrantGeneratedQrLoginProgress.Starting) + assertThat(awaitItem()).isEqualTo(LinkMobileStep.Starting) + // generate returns, Done event is emitted + completable.complete(Unit) + assertThat(awaitItem()).isEqualTo(LinkMobileStep.Done) } } @@ -81,6 +118,61 @@ class RustLinkMobileHandlerTest { } } + @Test + fun `when start throws HumanQrGrantLoginException_NotFound when in state QrReady, the handler emits QrRotating step`() = runTest { + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { + completable.await() + throw HumanQrGrantLoginException.NotFound("Timeout") + } + ) + val sut = createRustLinkMobileHandler( + handler, + ) + sut.linkMobileStep.test { + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized) + backgroundScope.launch { + sut.start() + } + runCurrent() + handler.emitGenerateProgress(GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))) + val readyState = awaitItem() + assertThat(readyState).isInstanceOf(LinkMobileStep.QrReady::class.java) + // generate returns, error is emitted + completable.complete(Unit) + val qrRotatingState = awaitItem() + assertThat(qrRotatingState).isEqualTo(LinkMobileStep.QrRotating) + } + } + + @Test + fun `when start throws HumanQrGrantLoginException_Expired, the handler emits QrRotating step`() = runTest { + val completable = CompletableDeferred() + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { + completable.await() + throw HumanQrGrantLoginException.Expired("Expired") + } + ) + val sut = createRustLinkMobileHandler( + handler, + ) + sut.linkMobileStep.test { + val initialItem = awaitItem() + assertThat(initialItem).isEqualTo(LinkMobileStep.Uninitialized) + backgroundScope.launch { + sut.start() + } + runCurrent() + // generate returns, error is emitted + completable.complete(Unit) + val qrRotatingState = awaitItem() + assertThat(qrRotatingState).isEqualTo(LinkMobileStep.QrRotating) + } + } + private fun TestScope.createRustLinkMobileHandler( handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(), ) = RustLinkMobileHandler( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt index e5fc8b154f..6042f195f8 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/mapper/SessionKtTest.kt @@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.impl.mapper import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.auth.external.ExternalSession +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession -import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2 @@ -36,7 +36,7 @@ class SessionKtTest { assertThat(result.refreshToken).isEqualTo("refreshToken") assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL) assertThat(result.isTokenValid).isTrue() - assertThat(result.oidcData).isNull() + assertThat(result.oAuthData).isNull() assertThat(result.loginType).isEqualTo(LoginType.PASSWORD) assertThat(result.loginTimestamp).isNotNull() assertThat(result.passphrase).isEqualTo(A_SECRET) @@ -82,7 +82,7 @@ class SessionKtTest { assertThat(result.refreshToken).isNull() assertThat(result.homeserverUrl).isEqualTo(A_HOMESERVER_URL) assertThat(result.isTokenValid).isTrue() - assertThat(result.oidcData).isNull() + assertThat(result.oAuthData).isNull() assertThat(result.loginType).isEqualTo(LoginType.PASSWORD) assertThat(result.loginTimestamp).isNotNull() assertThat(result.passphrase).isEqualTo(A_SECRET) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementActionKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementActionKtTest.kt similarity index 90% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementActionKtTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementActionKtTest.kt index 3637ef78cc..1495716e87 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oidc/AccountManagementActionKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/oauth/AccountManagementActionKtTest.kt @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.matrix.impl.oidc +package io.element.android.libraries.matrix.impl.oauth import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction import io.element.android.libraries.matrix.test.A_DEVICE_ID import org.junit.Test import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManagementAction diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt index dcd2c4594c..97b51c0026 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt @@ -10,12 +10,10 @@ package io.element.android.libraries.matrix.impl.permalink import android.net.Uri import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultMatrixToConverterTest { +class DefaultMatrixToConverterTest : RobolectricTest() { @Test fun `converting a matrix-to url does nothing`() { val url = Uri.parse("https://matrix.to/#/#element-android:matrix.org") diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt index 61b2965cdd..b58c2985ff 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt @@ -48,6 +48,7 @@ private fun aSetHttpPusherData( deviceDisplayName: String = "deviceDisplayName", profileTag: String = "profileTag", lang: String = "lang", + append: Boolean = false, ) = SetHttpPusherData( pushKey = pushKey, appId = appId, @@ -56,7 +57,8 @@ private fun aSetHttpPusherData( appDisplayName = appDisplayName, deviceDisplayName = deviceDisplayName, profileTag = profileTag, - lang = lang + lang = lang, + append = append, ) private fun aUnsetHttpPusherData( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventFilterFactory.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventFilterFactory.kt index f35746d8d3..713e04d239 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventFilterFactory.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventFilterFactory.kt @@ -9,11 +9,12 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.matrix.api.room.StateEventType +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventFilter import org.matrix.rustcomponents.sdk.TimelineEventFilter class FakeTimelineEventFilterFactory : TimelineEventFilterFactory { - override fun create(listStateEventType: List): TimelineEventFilter { + override fun create(joinRule: JoinRule?, isEncrypted: Boolean?, excludedStateTypes: List): TimelineEventFilter { return FakeFfiTimelineEventFilter() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt index 86a50c3926..dd91595359 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoExtTest.kt @@ -20,8 +20,7 @@ class RoomInfoExtTest { @Test fun `get non empty element Heroes`() { val result = aRustRoomInfo( - isDirect = true, - activeMembersCount = 2uL, + isDm = true, heroes = listOf(aRustRoomHero()) ).elementHeroes() assertThat(result).isEqualTo( @@ -38,8 +37,7 @@ class RoomInfoExtTest { @Test fun `too many heroes and element Heroes is empty`() { val result = aRustRoomInfo( - isDirect = true, - activeMembersCount = 2uL, + isDm = true, heroes = listOf(aRustRoomHero(), aRustRoomHero()) ).elementHeroes() assertThat(result).isEmpty() @@ -48,18 +46,7 @@ class RoomInfoExtTest { @Test fun `not direct and element Heroes is empty`() { val result = aRustRoomInfo( - isDirect = false, - activeMembersCount = 2uL, - heroes = listOf(aRustRoomHero()) - ).elementHeroes() - assertThat(result).isEmpty() - } - - @Test - fun `too many members and element Heroes is empty`() { - val result = aRustRoomInfo( - isDirect = true, - activeMembersCount = 3uL, + isDm = false, heroes = listOf(aRustRoomHero()) ).elementHeroes() assertThat(result).isEmpty() diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapperTest.kt index ab353bc0f5..dc3b16ca3c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RoomInfoMapperTest.kt @@ -86,6 +86,8 @@ class RoomInfoMapperTest { privilegedCreatorsRole = true, isLowPriority = true, activeRoomCallConsensusIntent = RtcCallIntentConsensus.Full(RtcCallIntent.AUDIO), + isDm = true, + fullyReadEventId = AN_EVENT_ID.value, ) ) ).isEqualTo( @@ -136,6 +138,8 @@ class RoomInfoMapperTest { privilegedCreatorRole = true, isLowPriority = true, activeCallIntentConsensus = CallIntentConsensus.Full(CallIntent.AUDIO), + isDm = true, + fullyReadEventId = AN_EVENT_ID, ) ) } @@ -181,6 +185,8 @@ class RoomInfoMapperTest { privilegedCreatorsRole = true, isLowPriority = true, activeRoomCallConsensusIntent = RtcCallIntentConsensus.None, + isDm = false, + fullyReadEventId = AN_EVENT_ID.value, ) ) ).isEqualTo( @@ -225,6 +231,8 @@ class RoomInfoMapperTest { privilegedCreatorRole = true, isLowPriority = true, activeCallIntentConsensus = CallIntentConsensus.None, + isDm = false, + fullyReadEventId = AN_EVENT_ID, ) ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlowTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlowTest.kt index 41627396ad..ba91dae468 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlowTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/location/TimedLiveLocationSharesFlowTest.kt @@ -11,6 +11,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.location.LiveLocationShare +import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.emptyFlow @@ -24,9 +25,9 @@ class TimedLiveLocationSharesFlowTest { @Test fun `it keeps emitting shares for subsequent expiries without upstream changes`() = runTest { val shares = listOf( - aLiveLocationShare(userId = "@alice:server", endTimestamp = 1_000), - aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000), - aLiveLocationShare(userId = "@carol:server", endTimestamp = 3_000), + aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 1_000), + aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000), + aLiveLocationShare(userId = UserId("@carol:server"), endTimestamp = 3_000), ) flowOf(shares) @@ -56,8 +57,8 @@ class TimedLiveLocationSharesFlowTest { @Test fun `it does not double-emit when a share is already expired on receipt`() = runTest { val shares = listOf( - aLiveLocationShare(userId = "@alice:server", endTimestamp = 500), - aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000), + aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 500), + aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000), ) flowOf(shares) @@ -81,8 +82,8 @@ class TimedLiveLocationSharesFlowTest { val upstream = MutableSharedFlow>(extraBufferCapacity = 1) val initialShares = listOf(aLiveLocationShare(endTimestamp = 10_000)) val updatedShares = listOf( - aLiveLocationShare(userId = "@alice:server", endTimestamp = 10_000), - aLiveLocationShare(userId = "@bob:server", endTimestamp = 6_000), + aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 10_000), + aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 6_000), ) upstream @@ -133,15 +134,3 @@ class TimedLiveLocationSharesFlowTest { } } } - -private fun aLiveLocationShare( - userId: String = "@user:server", - endTimestamp: Long, -): LiveLocationShare { - return LiveLocationShare( - userId = UserId(userId), - lastLocation = null, - startTimestamp = 0L, - endTimestamp = endTimestamp, - ) -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapperTest.kt index f298da8b42..65f2d1e2d5 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/powerlevels/RoomPowerLevelsValuesMapperTest.kt @@ -30,6 +30,8 @@ class RoomPowerLevelsValuesMapperTest { roomAvatar = 9, roomTopic = 10, spaceChild = 11, + beacon = 12, + beaconInfo = 13, ) ) ).isEqualTo( @@ -44,6 +46,8 @@ class RoomPowerLevelsValuesMapperTest { roomAvatar = 9, roomTopic = 10, spaceChild = 11, + beacon = 12, + beaconInfo = 13, ) ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt index 6fac54b904..d951b3d339 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt @@ -173,16 +173,68 @@ class RoomSummaryListProcessorTest { assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3) } + /** + * Tracking issue #4182 / #5031: rooms duplicated in the room list. + * + * If duplicates are present in the upstream summaries flow, the dedupe safety net in + * [RoomSummaryListProcessor.updateRoomSummaries] must remove them and report the incident via + * [analyticsService.trackError]. Uses an empty update to drive the dedupe path without + * passing a Rust Room through the destroy-on-use path. + */ + @Test + fun `pre-existing duplicates in summaries are deduped on next update and trackError fires`() = runTest { + summaries.value = listOf( + aRoomSummary(roomId = A_ROOM_ID), + aRoomSummary(roomId = A_ROOM_ID), // simulated SDK-side leak + aRoomSummary(roomId = A_ROOM_ID_2), + ) + val analyticsService = FakeAnalyticsService() + val processor = createProcessor(analyticsService = analyticsService) + + processor.postUpdate(emptyList()) + + assertThat(summaries.value.map { it.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_2).inOrder() + assertThat(analyticsService.trackedErrors).hasSize(1) + } + + /** + * Tracking issue #4182 / #5031. + * + * Insert is the most likely Rust-SDK trigger for a duplicate-room report: it blindly inserts + * a new entry at an index without checking whether the roomId already exists. Before the + * describe-capture fix, the dedupe branch in [updateRoomSummaries] would call `Room.id()` + * on an already-destroyed Room (because [applyUpdate] consumes each value via + * `entry.use { ... }`) and crash before [trackError] could be invoked. This test guards the + * fix: the Insert is processed, the list is emitted deduplicated, and the tracked error + * message carries the human-readable description of the offending update. + */ + @Test + fun `Insert that triggers dedupe is reported via trackError without crashing`() = runTest { + summaries.value = listOf(aRoomSummary(roomId = A_ROOM_ID)) + val analyticsService = FakeAnalyticsService() + val processor = createProcessor(analyticsService = analyticsService) + + processor.postUpdate(listOf(RoomListEntriesUpdate.Insert(0u, aRustRoom(A_ROOM_ID)))) + + assertThat(summaries.value.map { it.roomId }).containsExactly(A_ROOM_ID) + assertThat(analyticsService.trackedErrors).hasSize(1) + val message = analyticsService.trackedErrors.single().message.orEmpty() + assertThat(message).contains("Found duplicates") + assertThat(message).contains("Insert at #0") + } + private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeFfiRoom( roomId = roomId, latestEventLambda = { LatestEventValue.None } ) - private fun TestScope.createProcessor() = RoomSummaryListProcessor( + private fun TestScope.createProcessor( + analyticsService: FakeAnalyticsService = FakeAnalyticsService(), + ) = RoomSummaryListProcessor( summaries, FakeFfiRoomListService(), coroutineContext = StandardTestDispatcher(testScheduler), roomSummaryFactory = RoomSummaryFactory(), - analyticsService = FakeAnalyticsService(), + analyticsService = analyticsService, ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilder.kt index 2f12587f5a..7eeff7db33 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilder.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilder.kt @@ -7,10 +7,11 @@ package io.element.android.libraries.matrix.impl.storage +import io.element.android.libraries.androidutils.crypto.ClientSecret import org.matrix.rustcomponents.sdk.ClientBuilder class FakeSqliteStoreBuilder : SqliteStoreBuilder { - override fun passphrase(passphrase: String?): SqliteStoreBuilder = this + override fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder = this override fun setupClientBuilder(clientBuilder: ClientBuilder): ClientBuilder { return clientBuilder diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilderProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilderProvider.kt index b196604ca4..af68da7b8a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilderProvider.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/storage/FakeSqliteStoreBuilderProvider.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.impl.storage -import io.element.android.libraries.matrix.impl.paths.SessionPaths +import io.element.android.libraries.matrix.api.paths.SessionPaths class FakeSqliteStoreBuilderProvider : SqliteStoreBuilderProvider { override fun provide(sessionPaths: SessionPaths): SqliteStoreBuilder { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt index 50f8637096..cda5a07b8e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/Fixtures.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime 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.timeline.aMessageContent +import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent @@ -36,6 +37,14 @@ internal val otherMemberJoinEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.member_other"), event = anEventTimelineItem(content = aRoomMembershipContent(userId = A_USER_ID_2, change = MembershipChange.JOINED)) ) +internal val otherMemberLeaveEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.member_leave"), + event = anEventTimelineItem(content = aRoomMembershipContent(userId = A_USER_ID_2, change = MembershipChange.LEFT)) +) +internal val profileChangeEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("m.room.member_profile"), + event = anEventTimelineItem(content = aProfileChangeMessageContent(displayName = "New Name", prevDisplayName = "Old Name")) +) internal val messageEvent = MatrixTimelineItem.Event( uniqueId = UniqueId("m.room.message"), event = anEventTimelineItem(content = aMessageContent("hi")) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index dbeba39973..680422827c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -96,7 +96,12 @@ class RoomBeginningPostProcessorTest { messageEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false) + val processedItems = processor.process( + timelineItems, + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = false + ) assertThat(processedItems).isEqualTo(expected) } @@ -107,7 +112,12 @@ class RoomBeginningPostProcessorTest { roomCreatorJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) + val processedItems = processor.process( + timelineItems, + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = true + ) assertThat(processedItems).isEmpty() } @@ -117,7 +127,12 @@ class RoomBeginningPostProcessorTest { roomCreatorJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) + val processedItems = processor.process( + timelineItems, + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = true + ) assertThat(processedItems).isEmpty() } @@ -128,7 +143,12 @@ class RoomBeginningPostProcessorTest { otherMemberJoinEvent, ) val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true) + val processedItems = processor.process( + timelineItems, + isDm = true, + roomCreator = A_USER_ID, + hasMoreToLoadBackwards = true + ) assertThat(processedItems).isEqualTo(listOf(otherMemberJoinEvent)) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt deleted file mode 100644 index 8edc95cdc6..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/util/SessionPathsProviderTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.util - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.sessionstorage.test.InMemorySessionStore -import io.element.android.libraries.sessionstorage.test.aSessionData -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class SessionPathsProviderTest { - @Test - fun `if session is not found, provides returns null`() = runTest { - val sut = SessionPathsProvider(InMemorySessionStore()) - val result = sut.provides(A_SESSION_ID) - assertThat(result).isNull() - } - - @Test - fun `if session is found, provides returns the data`() = runTest { - val store = InMemorySessionStore( - initialList = listOf( - aSessionData( - sessionPath = "/a/path/to/a/session", - cachePath = "/a/path/to/a/cache", - ) - ) - ) - val sut = SessionPathsProvider(store) - val result = sut.provides(A_SESSION_ID)!! - assertThat(result.fileDirectory.absolutePath).isEqualTo("/a/path/to/a/session") - assertThat(result.cacheDirectory.absolutePath).isEqualTo("/a/path/to/a/cache") - } -} diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index 63836d857a..ccfa56f1aa 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.coroutines.test) + implementation(projects.libraries.architecture) 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/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 742af160ae..cce99e9d09 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 @@ -26,7 +26,8 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.oidc.AccountManagementAction +import io.element.android.libraries.matrix.api.oauth.AccountManagementAction +import io.element.android.libraries.matrix.api.paths.SessionPaths import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -34,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.NotJoinedRoom import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias +import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.spaces.SpaceService @@ -65,10 +67,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import java.io.File import java.util.Optional class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, + override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")), override val deviceId: DeviceId = A_DEVICE_ID, override val sessionCoroutineScope: CoroutineScope = TestScope(), private val userDisplayName: String? = A_USER_NAME, @@ -107,12 +111,14 @@ class FakeMatrixClient( private val canReportRoomLambda: () -> Boolean = { false }, private val isLivekitRtcSupportedLambda: () -> Boolean = { false }, override val ignoredUsersFlow: StateFlow> = MutableStateFlow(persistentListOf()), + override val ownBeaconInfoUpdates: Flow = emptyFlow(), private val getMaxUploadSizeResult: () -> Result = { lambdaError() }, private val getJoinedRoomIdsResult: () -> Result> = { Result.success(emptySet()) }, private val getRecentEmojisLambda: () -> Result> = { Result.success(emptyList()) }, private val addRecentEmojiLambda: (String) -> Result = { Result.success(Unit) }, private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result = { _, _ -> lambdaError() }, private val performDatabaseVacuumLambda: () -> Result = { lambdaError() }, + private val getMapStyleUrlResult: () -> Result = { lambdaError() }, private val getDatabaseSizesLambda: () -> Result = { lambdaError() }, private val resetWellKnownConfigLambda: () -> Result = { lambdaError() }, ) : MatrixClient { @@ -371,6 +377,10 @@ class FakeMatrixClient( return performDatabaseVacuumLambda() } + override suspend fun getMapStyleUrl(): Result = simulateLongTask { + getMapStyleUrlResult() + } + override suspend fun canLinkNewDevice(): Result = simulateLongTask { return canLinkNewDeviceResult() } 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 238ad2663d..3b2fdeafb1 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 @@ -12,8 +12,8 @@ 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 -import io.element.android.libraries.matrix.api.auth.OidcPrompt +import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.matrix.api.auth.OAuthPrompt 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 @@ -26,7 +26,7 @@ import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.simulateLongTask -val A_OIDC_DATA = OidcDetails(url = "a-url") +val AN_OAUTH_DATA = OAuthDetails(url = "a-url") class FakeMatrixAuthenticationService( var matrixClientResult: ((SessionId) -> Result)? = null, @@ -37,8 +37,8 @@ class FakeMatrixAuthenticationService( 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 + private var oAuthError: Throwable? = null + private var oAuthCancelError: Throwable? = null private var loginError: Throwable? = null private var matrixClient: MatrixClient? = null private var onAuthenticationListener: ((MatrixClient) -> Unit)? = null @@ -70,18 +70,18 @@ class FakeMatrixAuthenticationService( return importCreatedSessionLambda(externalSession) } - override suspend fun getOidcUrl( - prompt: OidcPrompt, + override suspend fun getOAuthUrl( + prompt: OAuthPrompt, loginHint: String?, - ): Result = simulateLongTask { - oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) + ): Result = simulateLongTask { + oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA) } - override suspend fun cancelOidcLogin(): Result { - return oidcCancelError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun cancelOAuthLogin(): Result { + return oAuthCancelError?.let { Result.failure(it) } ?: Result.success(Unit) } - override suspend fun loginWithOidc(callbackUrl: String): Result = simulateLongTask { + override suspend fun loginWithOAuth(callbackUrl: String): Result = simulateLongTask { loginError?.let { Result.failure(it) } ?: run { onAuthenticationListener?.invoke(matrixClient ?: FakeMatrixClient()) Result.success(A_USER_ID) @@ -97,12 +97,12 @@ class FakeMatrixAuthenticationService( onAuthenticationListener = lambda } - fun givenOidcError(throwable: Throwable?) { - oidcError = throwable + fun givenOAuthError(throwable: Throwable?) { + oAuthError = throwable } - fun givenOidcCancelError(throwable: Throwable?) { - oidcCancelError = throwable + fun givenOAuthCancelError(throwable: Throwable?) { + oAuthCancelError = throwable } fun givenLoginError(throwable: Throwable?) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOAuthRedirectUrlProvider.kt similarity index 75% rename from libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt rename to libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOAuthRedirectUrlProvider.kt index 47c9b0951d..fcb28b48ba 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOidcRedirectUrlProvider.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeOAuthRedirectUrlProvider.kt @@ -8,12 +8,12 @@ package io.element.android.libraries.matrix.test.auth -import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider +import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider const val FAKE_REDIRECT_URL = "io.element.android:/" -class FakeOidcRedirectUrlProvider( +class FakeOAuthRedirectUrlProvider( private val provideResult: String = FAKE_REDIRECT_URL, -) : OidcRedirectUrlProvider { +) : OAuthRedirectUrlProvider { override fun provide() = provideResult } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/MatrixHomeServerDetails.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/MatrixHomeServerDetails.kt index 3b9573bcfc..56fd4de4c3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/MatrixHomeServerDetails.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/MatrixHomeServerDetails.kt @@ -14,9 +14,9 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL fun aMatrixHomeServerDetails( url: String = A_HOMESERVER_URL, supportsPasswordLogin: Boolean = false, - supportsOidcLogin: Boolean = false, + supportsOAuthLogin: Boolean = false, ) = MatrixHomeServerDetails( url = url, supportsPasswordLogin = supportsPasswordLogin, - supportsOidcLogin = supportsOidcLogin, + supportsOAuthLogin = supportsOAuthLogin, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index 04e3779298..e090319821 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -28,7 +28,8 @@ class FakeEncryptionService( private val pinUserIdentityResult: (UserId) -> Result = { lambdaError() }, private val withdrawVerificationResult: (UserId) -> Result = { lambdaError() }, private val getUserIdentityResult: (UserId) -> Result = { lambdaError() }, - private val enableRecoveryLambda: (Boolean) -> Result = { lambdaError() }, + private val enableRecoveryLambda: (Boolean, String?) -> Result = { _, _ -> lambdaError() }, + private val resetRecoveryKeyLambda: () -> Result = { Result.success(FAKE_RECOVERY_KEY) }, ) : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) @@ -90,11 +91,11 @@ class FakeEncryptionService( } override suspend fun resetRecoveryKey(): Result = simulateLongTask { - return Result.success(FAKE_RECOVERY_KEY) + return resetRecoveryKeyLambda() } - override suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result = simulateLongTask { - return enableRecoveryLambda(waitForBackupsToUpload) + override suspend fun enableRecovery(waitForBackupsToUpload: Boolean, passphrase: String?): Result = simulateLongTask { + return enableRecoveryLambda(waitForBackupsToUpload, passphrase) } fun givenWaitForBackupUploadSteadyStateFlow(flow: Flow) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt index 06ffeb547c..d65328d7fe 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt @@ -8,16 +8,16 @@ package io.element.android.libraries.matrix.test.encryption -import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle +import io.element.android.libraries.matrix.api.encryption.IdentityOAuthResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle -class FakeIdentityOidcResetHandle( +class FakeIdentityOAuthResetHandle( override val url: String = "", - var resetOidcLambda: () -> Result = { error("Not implemented") }, + var resetOAuthLambda: () -> Result = { error("Not implemented") }, var cancelLambda: () -> Unit = { error("Not implemented") }, -) : IdentityOidcResetHandle { - override suspend fun resetOidc(): Result { - return resetOidcLambda() +) : IdentityOAuthResetHandle { + override suspend fun resetOAuth(): Result { + return resetOAuthLambda() } override suspend fun cancel() { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt index 564cd231b2..f7970e7971 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt @@ -70,12 +70,12 @@ class FakeNotificationSettingsService( } } - override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result { + override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isDM: Boolean): Result { val error = setDefaultNotificationModeError if (error != null) { return Result.failure(error) } - if (isOneToOne) { + if (isDM) { if (isEncrypted) { defaultEncryptedOneToOneRoomNotificationMode = mode } else { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index 4ceddc414c..8f5d626b13 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -186,10 +186,6 @@ class FakeBaseRoom( membersStateFlow.value = state } - override suspend fun clearEventCacheStorage(): Result { - return Result.success(Unit) - } - override suspend fun reportRoom(reason: String?) = reportRoomResult(reason) override suspend fun declineCall(notificationEventId: EventId): Result { @@ -223,4 +219,6 @@ fun defaultRoomPowerLevelValues() = RoomPowerLevelsValues( roomAvatar = 50, roomTopic = 50, spaceChild = 50, + beacon = 0, + beaconInfo = 0, ) 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 84497b38de..dd8acacb23 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 @@ -34,6 +34,7 @@ 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 import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import io.element.android.libraries.matrix.test.AN_EVENT_ID 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 @@ -88,9 +89,10 @@ class FakeJoinedRoom( private val updateJoinRuleResult: (JoinRule) -> Result = { lambdaError() }, private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> }, private val liveLocationSharesFlow: Flow> = MutableStateFlow(emptyList()), - private val startLiveLocationShareResult: (Long) -> Result = { lambdaError() }, + private val startLiveLocationShareResult: (Long) -> Result = { lambdaError() }, private val stopLiveLocationShareResult: () -> Result = { lambdaError() }, private val sendLiveLocationResult: (String) -> Result = { lambdaError() }, + private val setOwnMemberDisplayNameResult: (String) -> Result = { lambdaError() }, ) : JoinedRoom, BaseRoom by baseRoom { private val sendQueueUpdates = MutableSharedFlow(extraBufferCapacity = 10) @@ -133,7 +135,11 @@ class FakeJoinedRoom( } override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { - val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings(roomId, info().isEncrypted.orFalse(), isOneToOne).getOrThrow() + val notificationSettings = roomNotificationSettingsService.getRoomNotificationSettings( + roomId = roomId, + isEncrypted = info().isEncrypted.orFalse(), + isOneToOne = isDm(), + ).getOrThrow() (roomNotificationSettingsStateFlow as MutableStateFlow).value = RoomNotificationSettingsState.Ready(notificationSettings) return Result.success(Unit) } @@ -238,8 +244,8 @@ class FakeJoinedRoom( return liveLocationSharesFlow } - override suspend fun startLiveLocationShare(durationMillis: Long): Result = simulateLongTask { - startLiveLocationShareResult(durationMillis) + override suspend fun startLiveLocationShare(durationMillis: Long): Result = simulateLongTask { + startLiveLocationShareResult(durationMillis).map { AN_EVENT_ID } } override suspend fun stopLiveLocationShare(): Result = simulateLongTask { @@ -250,6 +256,10 @@ class FakeJoinedRoom( sendLiveLocationResult(geoUri) } + override suspend fun setOwnMemberDisplayName(displayName: String): Result = simulateLongTask { + setOwnMemberDisplayNameResult(displayName) + } + private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) { progressCallbackValues.forEach { (current, total) -> progressCallback?.onProgress(current, total) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt index c7faaba627..3293570eec 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt @@ -71,6 +71,8 @@ fun aRoomInfo( privilegedCreatorRole: Boolean = false, isLowPriority: Boolean = false, activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None, + isDm: Boolean = false, + fullyReadEventId: EventId? = null, ) = RoomInfo( id = id, name = name, @@ -109,4 +111,6 @@ fun aRoomInfo( privilegedCreatorRole = privilegedCreatorRole, isLowPriority = isLowPriority, activeCallIntentConsensus = activeCallIntentConsensus, + isDm = isDm, + fullyReadEventId = fullyReadEventId, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt index f6bc0c5ec2..33a1b68ba3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt @@ -23,6 +23,7 @@ fun aRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, + isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -33,6 +34,7 @@ fun aRoomMember( isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, + isServiceMember = isServiceMember, ) fun aRoomMemberList() = persistentListOf( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index afe4c88f5a..392aa880c9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -81,6 +81,7 @@ fun aRoomSummary( privilegedCreatorRole: Boolean = false, isLowPriority: Boolean = false, activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None, + fullyReadEventId: EventId? = null, ) = RoomSummary( info = RoomInfo( id = roomId, @@ -120,6 +121,8 @@ fun aRoomSummary( privilegedCreatorRole = privilegedCreatorRole, isLowPriority = isLowPriority, activeCallIntentConsensus = activeCallIntentConsensus, + isDm = false, + fullyReadEventId = fullyReadEventId, ), latestEvent = latestEvent, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/location/LiveLocationFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/location/LiveLocationFixture.kt new file mode 100644 index 0000000000..23730c8886 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/location/LiveLocationFixture.kt @@ -0,0 +1,38 @@ +/* + * 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.location + +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.location.AssetType +import io.element.android.libraries.matrix.api.room.location.LastLocation +import io.element.android.libraries.matrix.api.room.location.LiveLocationShare +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID + +fun aLiveLocationShare( + beaconId: EventId = AN_EVENT_ID, + userId: UserId = A_USER_ID, + geoUri: String = "geo:48.8584,2.2945", + timestamp: Long = 0L, + startTimestamp: Long = 0L, + endTimestamp: Long = Long.MAX_VALUE, + assetType: AssetType = AssetType.SENDER, +): LiveLocationShare { + return LiveLocationShare( + beaconId = beaconId, + userId = userId, + lastLocation = LastLocation( + geoUri = geoUri, + timestamp = timestamp, + assetType = assetType, + ), + startTimestamp = startTimestamp, + endTimestamp = endTimestamp, + ) +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index f4f12a435e..0aeeebf86d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -22,12 +22,12 @@ import kotlinx.coroutines.flow.StateFlow class FakeSessionVerificationService( initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown, - private val requestCurrentSessionVerificationLambda: () -> Unit = { lambdaError() }, + private val requestDeviceVerificationLambda: () -> Unit = { lambdaError() }, private val requestUserVerificationLambda: (UserId) -> Unit = { lambdaError() }, private val cancelVerificationLambda: () -> Unit = { lambdaError() }, private val approveVerificationLambda: () -> Unit = { lambdaError() }, private val declineVerificationLambda: () -> Unit = { lambdaError() }, - private val startVerificationLambda: () -> Unit = { lambdaError() }, + private val startSasVerificationLambda: () -> Unit = { lambdaError() }, private val resetLambda: (Boolean) -> Unit = { lambdaError() }, private val acknowledgeVerificationRequestLambda: (VerificationRequest.Incoming) -> Unit = { lambdaError() }, private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() }, @@ -40,31 +40,31 @@ class FakeSessionVerificationService( override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus override val needsSessionVerification: Flow = _needsSessionVerification - override suspend fun requestCurrentSessionVerification() { - requestCurrentSessionVerificationLambda() + override suspend fun requestDeviceVerification() = simulateLongTask { + requestDeviceVerificationLambda() } - override suspend fun requestUserVerification(userId: UserId) { + override suspend fun requestUserVerification(userId: UserId) = simulateLongTask { requestUserVerificationLambda(userId) } - override suspend fun cancelVerification() { + override suspend fun cancelVerification() = simulateLongTask { cancelVerificationLambda() } - override suspend fun approveVerification() { + override suspend fun approveVerification() = simulateLongTask { approveVerificationLambda() } - override suspend fun declineVerification() { + override suspend fun declineVerification() = simulateLongTask { declineVerificationLambda() } - override suspend fun startVerification() { - startVerificationLambda() + override suspend fun startSasVerification() = simulateLongTask { + startSasVerificationLambda() } - override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) { + override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) = simulateLongTask { resetLambda(cancelAnyPendingVerificationAttempt) } @@ -75,7 +75,7 @@ class FakeSessionVerificationService( this.listener = listener } - override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) { + override suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) = simulateLongTask { acknowledgeVerificationRequestLambda(verificationRequest) } diff --git a/libraries/matrixmedia/impl/build.gradle.kts b/libraries/matrixmedia/impl/build.gradle.kts index 82afc2f62c..56ccc79afc 100644 --- a/libraries/matrixmedia/impl/build.gradle.kts +++ b/libraries/matrixmedia/impl/build.gradle.kts @@ -19,8 +19,10 @@ android { setupDependencyInjection() dependencies { + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.designsystem) implementation(libs.coil.compose) implementation(libs.coil.gif) diff --git a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index e67e630e97..ef654a4cf7 100644 --- a/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixmedia/impl/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -16,7 +16,6 @@ import coil3.gif.GifDecoder import coil3.network.okhttp.OkHttpNetworkFetcherFactory import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Provider import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import okhttp3.OkHttpClient @@ -29,7 +28,7 @@ interface ImageLoaderFactory { @ContributesBinding(AppScope::class) class DefaultImageLoaderFactory( @ApplicationContext private val context: Context, - private val okHttpClient: Provider, + private val okHttpClient: () -> OkHttpClient, ) : ImageLoaderFactory { private val okHttpNetworkFetcherFactory = OkHttpNetworkFetcherFactory( callFactory = { diff --git a/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt b/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt index dd9127a4ae..2bb1ddcae4 100644 --- a/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt +++ b/libraries/matrixmedia/impl/src/test/kotlin/io/element/android/libraries/matrix/ui/media/DefaultImageLoaderHolderTest.kt @@ -19,13 +19,11 @@ import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObse import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultImageLoaderHolderTest { +class DefaultImageLoaderHolderTest : RobolectricTest() { @Test fun `get - returns the same ImageLoader for the same client`() { val context = InstrumentationRegistry.getInstrumentation().context diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt index 880a7a9df6..32899a8d2c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt @@ -104,7 +104,7 @@ private fun AvatarActionBottomSheetContent( leadingContent = ListItemContent.Icon(IconSource.Resource(action.iconResourceId)), style = when { action.destructive -> ListItemStyle.Destructive - else -> ListItemStyle.Primary + else -> ListItemStyle.Default } ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt index e743a2447c..81822a04cc 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarPickerView.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -183,6 +184,7 @@ private fun PickButton( } } +@Suppress("ModifierClickableOrder") @Composable private fun BoxScope.OverlayEditButton( onClick: () -> Unit, @@ -196,7 +198,10 @@ private fun BoxScope.OverlayEditButton( .clip(CircleShape) .clickable(interactionSource = interactionSource, onClick = onClick, indication = null) .background(ElementTheme.colors.bgCanvasDefault) - .border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape), + .border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape) + .clearAndSetSemantics { + hideFromAccessibility() + }, contentAlignment = Alignment.Center, ) { Icon( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/InviteSenderView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/InviteSenderView.kt index 13747a1a3d..12fa54add1 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/InviteSenderView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/InviteSenderView.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.InviteSender @@ -57,10 +58,10 @@ internal fun InviteSenderViewPreview() = ElementPreview { InviteSenderView( inviteSender = InviteSender( userId = UserId("@bob:example.com"), - displayName = "Bob", + displayName = USER_NAME_BOB, avatarData = AvatarData( id = "@bob:example.com", - name = "Bob", + name = USER_NAME_BOB, url = null, size = AvatarSize.InviteSender, ), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt index 5e9f29496e..639bafbcbf 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt @@ -9,6 +9,17 @@ package io.element.android.libraries.matrix.ui.components import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID +import io.element.android.libraries.designsystem.preview.USER_NAME_EVE +import io.element.android.libraries.designsystem.preview.USER_NAME_JOHN_DOE +import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN +import io.element.android.libraries.designsystem.preview.USER_NAME_MALLORY +import io.element.android.libraries.designsystem.preview.USER_NAME_SUSIE +import io.element.android.libraries.designsystem.preview.USER_NAME_VICTOR +import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -23,30 +34,30 @@ open class MatrixUserProvider : PreviewParameterProvider { open class MatrixUserWithAvatarProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aMatrixUser(displayName = "John Doe"), - aMatrixUser(displayName = "John Doe", avatarUrl = "anUrl"), + aMatrixUser(displayName = USER_NAME_JOHN_DOE), + aMatrixUser(displayName = USER_NAME_JOHN_DOE, avatarUrl = "anUrl"), ) } fun aMatrixUser( - id: String = "@id_of_alice:server.org", - displayName: String? = "Alice", + id: String? = null, + displayName: String? = USER_NAME_ALICE, avatarUrl: String? = null, ) = MatrixUser( - userId = UserId(id), + userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"), displayName = displayName, avatarUrl = avatarUrl, ) fun aMatrixUserList() = listOf( - aMatrixUser("@alice:server.org", "Alice"), - aMatrixUser("@bob:server.org", "Bob"), - aMatrixUser("@carol:server.org", "Carol"), - aMatrixUser("@david:server.org", "David"), - aMatrixUser("@eve:server.org", "Eve"), - aMatrixUser("@justin:server.org", "Justin"), - aMatrixUser("@mallory:server.org", "Mallory"), - aMatrixUser("@susie:server.org", "Susie"), - aMatrixUser("@victor:server.org", "Victor"), - aMatrixUser("@walter:server.org", "Walter"), + aMatrixUser(displayName = USER_NAME_ALICE), + aMatrixUser(displayName = USER_NAME_BOB), + aMatrixUser(displayName = USER_NAME_CAROL), + aMatrixUser(displayName = USER_NAME_DAVID), + aMatrixUser(displayName = USER_NAME_EVE), + aMatrixUser(displayName = USER_NAME_JUSTIN), + aMatrixUser(displayName = USER_NAME_MALLORY), + aMatrixUser(displayName = USER_NAME_SUSIE), + aMatrixUser(displayName = USER_NAME_VICTOR), + aMatrixUser(displayName = USER_NAME_WALTER), ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt index 374ebd7a1a..0a1e3bdc87 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/OrganizationHeader.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType 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 +import io.element.android.libraries.designsystem.preview.SPACE_NAME /** * Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2048&m=dev @@ -72,7 +73,7 @@ internal fun OrganizationHeaderPreview() = ElementPreview { url = "anUrl", size = AvatarSize.OrganizationHeader, ), - name = "Space name", + name = SPACE_NAME, numberOfSpaces = 9, numberOfRooms = 88, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index b118dbcbe3..17ec7c6ec7 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_JOHN_DOE import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName @@ -58,7 +59,7 @@ internal fun SelectedUserRtlPreview() = CompositionLocalProvider( ) { ElementPreview { SelectedUser( - matrixUser = aMatrixUser(displayName = "John Doe"), + matrixUser = aMatrixUser(displayName = USER_NAME_JOHN_DOE), canRemove = true, onUserRemove = {}, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt index c3e8292147..7aee62902f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt @@ -30,6 +30,11 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType 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 +import io.element.android.libraries.designsystem.preview.SPACE_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CHARLIE +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility import io.element.android.libraries.matrix.api.user.MatrixUser @@ -115,15 +120,15 @@ internal fun SpaceHeaderViewPreview() = ElementPreview { size = AvatarSize.SpaceHeader, ), alias = RoomAlias("#spaceAlias:matrix.org"), - name = "Space name", + name = SPACE_NAME, topic = "Space topic: " + LoremIpsum(40).values.first(), topicMaxLines = 2, visibility = SpaceRoomVisibility.Public, heroes = persistentListOf( - aMatrixUser(id = "@1:d", displayName = "Alice", avatarUrl = "aUrl"), - aMatrixUser(id = "@2:d", displayName = "Bob"), - aMatrixUser(id = "@3:d", displayName = "Charlie", avatarUrl = "aUrl"), - aMatrixUser(id = "@4:d", displayName = "Dave"), + aMatrixUser(id = "@1:d", displayName = USER_NAME_ALICE, avatarUrl = "aUrl"), + aMatrixUser(id = "@2:d", displayName = USER_NAME_BOB), + aMatrixUser(id = "@3:d", displayName = USER_NAME_CHARLIE, avatarUrl = "aUrl"), + aMatrixUser(id = "@4:d", displayName = USER_NAME_DAVID), ), numberOfMembers = 999, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt index 202ea79d87..6d927989d5 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceMembersView.kt @@ -22,6 +22,10 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CHARLIE +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.user.MatrixUser @@ -98,10 +102,10 @@ internal fun SpaceMembersViewPreview() = ElementPreview( ) { SpaceMembersView( heroes = persistentListOf( - aMatrixUser(id = "@1:d", displayName = "Alice", avatarUrl = "aUrl"), - aMatrixUser(id = "@2:d", displayName = "Bob"), - aMatrixUser(id = "@3:d", displayName = "Charlie", avatarUrl = "aUrl"), - aMatrixUser(id = "@4:d", displayName = "Dave"), + aMatrixUser(id = "@1:d", displayName = USER_NAME_ALICE, avatarUrl = "aUrl"), + aMatrixUser(id = "@2:d", displayName = USER_NAME_BOB), + aMatrixUser(id = "@3:d", displayName = USER_NAME_CHARLIE, avatarUrl = "aUrl"), + aMatrixUser(id = "@4:d", displayName = USER_NAME_DAVID), ), numberOfMembers = 123, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index 4d79018e88..d3824f9eec 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -78,7 +78,7 @@ fun SpaceRoomItemView( indication = ripple(), interactionSource = remember { MutableInteractionSource() } ) - .onKeyboardContextMenuAction { onLongClick } + .onKeyboardContextMenuAction(onLongClick) Column( modifier = modifier .then(clickModifier) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt index db63bba779..d6a582d83c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt @@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.ui.components import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.SPACE_NAME +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.RoomType @@ -28,10 +30,10 @@ class SpaceRoomProvider : PreviewParameterProvider { state = CurrentUserMembership.LEFT, ), aSpaceRoom( - displayName = "Alice", + displayName = SPACE_NAME, roomType = RoomType.Room, isDirect = true, - heroes = listOf(aMatrixUser(displayName = "Alice")), + heroes = listOf(aMatrixUser(displayName = USER_NAME_ALICE)), state = CurrentUserMembership.JOINED, numJoinedMembers = 2, ), @@ -69,9 +71,9 @@ class SpaceRoomProvider : PreviewParameterProvider { state = CurrentUserMembership.INVITED, ), aSpaceRoom( - displayName = "Alice", + displayName = SPACE_NAME, roomType = RoomType.Space, - heroes = listOf(aMatrixUser(displayName = "Alice")), + heroes = listOf(aMatrixUser(displayName = USER_NAME_ALICE)), state = CurrentUserMembership.JOINED, numJoinedMembers = 2, ), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt index ee9de51681..d29a28cba9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt @@ -12,8 +12,10 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat -import io.element.android.wysiwyg.utils.HtmlToDomParser +import org.jsoup.Jsoup import org.jsoup.nodes.Document +import org.jsoup.nodes.Document.OutputSettings +import org.jsoup.safety.Safelist /** * Converts the HTML string [FormattedBody.body] to a [Document] by parsing it. @@ -34,9 +36,9 @@ fun FormattedBody.toHtmlDocument( ?.trimEnd() ?.let { formattedBody -> val dom = if (prefix != null) { - HtmlToDomParser.document("$prefix $formattedBody") + CustomHtmlToDomParser.document("$prefix $formattedBody") } else { - HtmlToDomParser.document(formattedBody) + CustomHtmlToDomParser.document(formattedBody) } // Prepend `@` to mentions @@ -60,3 +62,35 @@ private fun fixMentions( } } } + +/** Custom Html to DOM parser, based on the one included in the rich text editor library. */ +private object CustomHtmlToDomParser { + fun document(html: String): Document { + val outputSettings = OutputSettings().prettyPrint(false).indentAmount(0) + val cleanHtml = Jsoup.clean(html, "", safeList, outputSettings) + return Jsoup.parse(cleanHtml) + } + + private val safeList = Safelist() + .addTags( + "a", + "b", + "strong", + "i", + "em", + "u", + "del", + "code", + "ul", + "ol", + "li", + "pre", + "blockquote", + "p", + "br", + // Add custom `mx-reply` tag, even if it's just to remove its contents from the plain text version of the message + "mx-reply" + ) + .addAttributes("a", "href", "data-mention-type", "contenteditable") + .addAttributes("ol", "start") +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt index d58d12b785..cf8b03b80d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.ui.messages import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat +import io.element.android.libraries.matrix.api.timeline.item.event.MessageTypeWithAttachment import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -26,6 +27,19 @@ fun TextMessageType.toPlainText( permalinkParser: PermalinkParser, ) = formatted?.toPlainText(permalinkParser) ?: body +/** + * Converts the HTML string in [MessageTypeWithAttachment.formattedCaption] to a plain text representation by parsing it and removing all formatting. + * If the caption is not formatted or the format is not [MessageFormat.HTML], the [MessageTypeWithAttachment.caption] is returned instead. + * If there is no caption, returns [default]. + */ +fun MessageTypeWithAttachment.toPlainText( + permalinkParser: PermalinkParser, + default: String = filename, +): String { + val plainTextFromFormatted = formattedCaption?.toPlainText(permalinkParser) + return plainTextFromFormatted ?: caption ?: default +} + /** * Converts the HTML string in [FormattedBody.body] to a plain text representation by parsing it and removing all formatting. * If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`. @@ -51,6 +65,8 @@ fun Document.toPlainText(): String { return visitor.build() } +private const val FALLBACK_REPLY_NODE_TAG = "mx-reply" + private class PlainTextNodeVisitor : NodeVisitor { private val builder = StringBuilder() @@ -78,6 +94,9 @@ private class PlainTextNodeVisitor : NodeVisitor { } else { builder.append("• ") } + } else if (node is Element && node.tagName() == FALLBACK_REPLY_NODE_TAG) { + // Remove the fallback reply node and its contents so they aren't added to the plain text message + node.remove() } else if (node is Element && node.isBlock && builder.lastOrNull() != '\n') { builder.append("\n") } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt index 5ddf57b723..ca118349e9 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.ui.messages.reply import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.USER_NAME_SENDER 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.media.MediaSource @@ -159,7 +160,7 @@ private fun aInReplyToDetails( ) fun aProfileDetailsReady( - displayName: String? = "Sender", + displayName: String? = USER_NAME_SENDER, displayNameAmbiguous: Boolean = false, avatarUrl: String? = null, ) = ProfileDetails.Ready( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt index 098117af74..f09447a083 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.State 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 io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.BaseRoom @@ -42,10 +43,8 @@ fun getRoomMemberAsState(roomMembersState: RoomMembersState, userId: UserId): St @Composable fun BaseRoom.getDirectRoomMember(roomMembersState: RoomMembersState): State { val roomInfo by roomInfoFlow.collectAsState() - return remember { - derivedStateOf { - roomMembersState.getDirectRoomMember(roomInfo, sessionId) - } + return remember(roomInfo.isDm) { + mutableStateOf(roomMembersState.getDirectRoomMember(roomInfo, sessionId)) } } diff --git a/libraries/matrixui/src/main/res/values-ca/translations.xml b/libraries/matrixui/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..44d3e10c55 --- /dev/null +++ b/libraries/matrixui/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "Envia invitació" + "Vols iniciar un xat amb %1$s?" + "Enviar invitació?" + "%1$s (%2$s) t\'ha convidat" + diff --git a/libraries/matrixui/src/main/res/values-da/translations.xml b/libraries/matrixui/src/main/res/values-da/translations.xml index 5b4970f003..f2dfa27073 100644 --- a/libraries/matrixui/src/main/res/values-da/translations.xml +++ b/libraries/matrixui/src/main/res/values-da/translations.xml @@ -3,5 +3,7 @@ "Send invitation" "Kunne du tænke dig at starte en samtale med %1$s?" "Send invitation?" + "Du har i øjeblikket ingen samtaler med denne person. Bekræft invitationen, før du fortsætter." + "Vil du starte en chat med denne nye kontakt?" "%1$s(%2$s ) inviterede dig" diff --git a/libraries/matrixui/src/main/res/values-et/translations.xml b/libraries/matrixui/src/main/res/values-et/translations.xml index abd3830768..a84990af10 100644 --- a/libraries/matrixui/src/main/res/values-et/translations.xml +++ b/libraries/matrixui/src/main/res/values-et/translations.xml @@ -3,6 +3,7 @@ "Saada kutse" "Kas sa soovid alustada vestlust kasutajaga %1$s?" "Kas saadame kutse?" + "Sul pole hetkel selle inimesega ühtegi vestlust. Enne jätkamist kinnita talle kutse saatmine." "Kas alustad vestlust selle uue kontaktiga?" "%1$s (%2$s) saatis sulle kutse" diff --git a/libraries/matrixui/src/main/res/values-it/translations.xml b/libraries/matrixui/src/main/res/values-it/translations.xml index 439d61337e..913c45867c 100644 --- a/libraries/matrixui/src/main/res/values-it/translations.xml +++ b/libraries/matrixui/src/main/res/values-it/translations.xml @@ -3,5 +3,7 @@ "Invia invito" "Vuoi iniziare una conversazione con%1$s?" "Inviare invito?" + "Al momento non hai alcuna conversazione con questa persona. Conferma l\'invito prima di continuare." + "Vuoi avviare una conversazione con questo nuovo contatto?" "%1$s (%2$s) ti ha invitato" diff --git a/libraries/matrixui/src/main/res/values-ko/translations.xml b/libraries/matrixui/src/main/res/values-ko/translations.xml index 09d831e348..b36a61cf9b 100644 --- a/libraries/matrixui/src/main/res/values-ko/translations.xml +++ b/libraries/matrixui/src/main/res/values-ko/translations.xml @@ -3,5 +3,7 @@ "초대장 보내기" "%1$s 와 채팅을 시작하시겠습니까?" "초대장을 보내시겠습니까?" + "이 사용자와의 대화 내역이 없습니다. 계속하려면 먼저 초대를 확인해 주세요." + "이 사용자와 대화를 시작하시겠습니까?" "%1$s (%2$s) 당신을 초대했습니다" diff --git a/libraries/matrixui/src/main/res/values-pl/translations.xml b/libraries/matrixui/src/main/res/values-pl/translations.xml index caa2c0e07d..bd31d7edb4 100644 --- a/libraries/matrixui/src/main/res/values-pl/translations.xml +++ b/libraries/matrixui/src/main/res/values-pl/translations.xml @@ -3,5 +3,7 @@ "Wyślij zaproszenie" "Czy chcesz rozpocząć czat z %1$s?" "Wysłać zaproszenie?" + "Obecnie nie posiadasz żadnych czatów z tą osobą. Potwierdź zaproszenie, zanim przejdziesz dalej." + "Rozpocząć czat z nowym kontaktem?" "%1$s (%2$s) zaprosił Cię" diff --git a/libraries/matrixui/src/main/res/values-ro/translations.xml b/libraries/matrixui/src/main/res/values-ro/translations.xml index 5156d6bd16..4d3eaa2364 100644 --- a/libraries/matrixui/src/main/res/values-ro/translations.xml +++ b/libraries/matrixui/src/main/res/values-ro/translations.xml @@ -3,5 +3,7 @@ "Trimiteți invitația" "Doriți să începeți o discuție cu %1$s?" "Trimiteți invitația?" + "În prezent, nu aveți nicio chat cu această persoană. Confirmați invitația înainte de a continua." + "Începeți o conversație cu acest nou contact?" "%1$s (%2$s) v-a invitat." diff --git a/libraries/matrixui/src/main/res/values-ru/translations.xml b/libraries/matrixui/src/main/res/values-ru/translations.xml index 8f01b9ee7f..77b3c6454e 100644 --- a/libraries/matrixui/src/main/res/values-ru/translations.xml +++ b/libraries/matrixui/src/main/res/values-ru/translations.xml @@ -5,5 +5,5 @@ "Отправить приглашение?" "У Вас нет других чатов с этим пользователем. Подтвердите, что это действительно кого Вы хотите пригласить, прежде чем продолжить." "Начать чат с этим новым контактом?" - "%1$s (%2$s) пригласил(а) вас" + "%1$s (%2$s) пригласил(а) Вас" diff --git a/libraries/matrixui/src/main/res/values-uk/translations.xml b/libraries/matrixui/src/main/res/values-uk/translations.xml index 324bb96086..4d2063fbd2 100644 --- a/libraries/matrixui/src/main/res/values-uk/translations.xml +++ b/libraries/matrixui/src/main/res/values-uk/translations.xml @@ -3,5 +3,7 @@ "Надіслати запрошення" "Хочете розпочати бесіду з %1$s?" "Надіслати запрошення?" + "Наразі у вас немає чатів із цим користувачем. Підтвердьте запрошення, перш ніж продовжити." + "Розпочати чат із цим новим контактом?" "%1$s (%2$s) запрошує вас" diff --git a/libraries/matrixui/src/main/res/values-zh/translations.xml b/libraries/matrixui/src/main/res/values-zh/translations.xml index b466f13226..e05e21095d 100644 --- a/libraries/matrixui/src/main/res/values-zh/translations.xml +++ b/libraries/matrixui/src/main/res/values-zh/translations.xml @@ -5,5 +5,5 @@ "发送邀请?" "你与此人暂无任何聊天。请确认对方被邀请后再继续。" "是否与新联系人开始聊天?" - "%1$s (%2$s)邀请了你" + "%1$s(%2$s)邀请了你" diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocumentTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocumentTest.kt index ad427999e2..ae336cd0d0 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocumentTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocumentTest.kt @@ -16,12 +16,10 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class ToHtmlDocumentTest { +class ToHtmlDocumentTest : RobolectricTest() { @Test fun `toHtmlDocument - returns null if format is not HTML`() { val body = FormattedBody( diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainTextTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainTextTest.kt index 607f825401..b1031176e7 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainTextTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainTextTest.kt @@ -13,13 +13,11 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.jsoup.Jsoup import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class ToPlainTextTest { +class ToPlainTextTest : RobolectricTest() { @Test fun `Document toPlainText - returns a plain text version of the document`() { val document = Jsoup.parse( @@ -136,4 +134,19 @@ class ToPlainTextTest { ) assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the fallback text") } + + @Test + fun `TextMessageType toPlainText - ignores mx-reply element`() { + val messageType = TextMessageType( + body = "This is the fallback text", + formatted = FormattedBody( + format = MessageFormat.HTML, + body = """ + In reply to... + This is the message content. + """.trimIndent() + ) + ) + assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the message content.") + } } diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index 5c7fdfaac8..0847315867 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.matrix.ui.messages.reply -import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test @@ -47,14 +46,13 @@ import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembers import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.withConfigurationAndContext import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith import kotlin.time.Duration.Companion.minutes -@RunWith(AndroidJUnit4::class) -class InReplyToMetadataKtTest { +class InReplyToMetadataKtTest : RobolectricTest() { @Test fun `any message content`() = runTest { moleculeFlow(RecompositionMode.Immediate) { diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt index dd9d801210..035548a4ec 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensionsTest.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.matrix.ui.model -import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test @@ -18,14 +17,13 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.withConfigurationAndContext import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class MatrixUserExtensionsTest { +class MatrixUserExtensionsTest : RobolectricTest() { @get:Rule val warmUpRule = WarmUpRule() diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/RoomMembersTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/RoomMembersTest.kt index 816ac0967e..737fff34ee 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/RoomMembersTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/RoomMembersTest.kt @@ -30,13 +30,10 @@ class RoomMembersTest { private val roomMember3 = aRoomMember(A_USER_ID_3) @Test - fun `getDirectRoomMember emits other member for encrypted DM with 2 joined members`() = runTest { + fun `getDirectRoomMember emits other member for encrypted DM`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo( - isDirect = true, - joinedMembersCount = 2, - ) + initialRoomInfo = aRoomInfo(isDm = true, isEncrypted = true) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -51,7 +48,7 @@ class RoomMembersTest { fun `getDirectRoomMember emit null if the room is not a dm`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDirect = false) + initialRoomInfo = aRoomInfo(isDm = false) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -66,10 +63,7 @@ class RoomMembersTest { fun `getDirectRoomMember emits other member even if the room is not encrypted`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo( - isDirect = true, - activeMembersCount = 2, - ) + initialRoomInfo = aRoomInfo(isDm = true) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -80,42 +74,11 @@ class RoomMembersTest { } } - @Test - fun `getDirectRoomMember emit null if the room has only 1 member`() = runTest { - val joinedRoom = FakeBaseRoom( - sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDirect = true) - ) - moleculeFlow(RecompositionMode.Immediate) { - joinedRoom.getDirectRoomMember( - RoomMembersState.Ready(persistentListOf(roomMember1)) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } - - @Test - fun `getDirectRoomMember emit null if the room has only 3 members`() = runTest { - val joinedRoom = FakeBaseRoom( - sessionId = A_USER_ID, - ).apply { - givenRoomInfo(aRoomInfo(isDirect = true, activeMembersCount = 3L)) - } - moleculeFlow(RecompositionMode.Immediate) { - joinedRoom.getDirectRoomMember( - RoomMembersState.Ready(persistentListOf(roomMember1, roomMember2, roomMember3)) - ) - }.test { - assertThat(awaitItem().value).isNull() - } - } - @Test fun `getDirectRoomMember emit null if the other member is not active`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo(isDirect = true), + initialRoomInfo = aRoomInfo(isDm = true), ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( @@ -135,10 +98,7 @@ class RoomMembersTest { fun `getDirectRoomMember emit the other member if there are 2 active members`() = runTest { val joinedRoom = FakeBaseRoom( sessionId = A_USER_ID, - initialRoomInfo = aRoomInfo( - isDirect = true, - activeMembersCount = 2, - ) + initialRoomInfo = aRoomInfo(isDm = true) ) moleculeFlow(RecompositionMode.Immediate) { joinedRoom.getDirectRoomMember( diff --git a/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt b/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt index 6c7dbb7db7..752398f7c4 100644 --- a/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt +++ b/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt @@ -13,12 +13,10 @@ import androidx.activity.result.contract.ActivityResultContracts import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.mediapickers.api.PickerType +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class PickerTypeTest { +class PickerTypeTest : RobolectricTest() { @Test fun `ImageAndVideo - assert types`() { val pickerType = PickerType.ImageAndVideo 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 57726ac5a2..5a06781aaa 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 @@ -29,20 +29,18 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Ignore import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import java.io.File import java.io.FileNotFoundException import java.io.IOException import kotlin.time.Duration -@RunWith(RobolectricTestRunner::class) -class AndroidMediaPreProcessorTest { +class AndroidMediaPreProcessorTest : RobolectricTest() { @Test fun `orientedImageDimensions swaps width and height for 90 degree exif orientation`() { val (width, height) = orientedImageDimensions( diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSenderTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSenderTest.kt index 139042804f..cfa3585629 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSenderTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/DefaultMediaSenderTest.kt @@ -25,18 +25,16 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import java.io.File -@RunWith(RobolectricTestRunner::class) -class DefaultMediaSenderTest { +class DefaultMediaSenderTest : RobolectricTest() { private val mediaOptimizationConfig = MediaOptimizationConfig( compressImages = true, videoCompressionPreset = VideoCompressionPreset.STANDARD, diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt index 041d05ae1e..6af0956541 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressorConfigFactoryTest.kt @@ -11,13 +11,10 @@ package io.element.android.libraries.mediaupload.impl import androidx.media3.transformer.VideoEncoderSettings import com.google.common.truth.Truth.assertThat import io.element.android.libraries.preferences.api.store.VideoCompressionPreset +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - @Suppress("NOTHING_TO_INLINE") -@RunWith(RobolectricTestRunner::class) -class VideoCompressorConfigFactoryTest { +class VideoCompressorConfigFactoryTest : RobolectricTest() { @Test fun `if we don't have metadata the video will be resized`() { // Given diff --git a/libraries/mediaupload/test/build.gradle.kts b/libraries/mediaupload/test/build.gradle.kts index 7e729089c7..f271ae1316 100644 --- a/libraries/mediaupload/test/build.gradle.kts +++ b/libraries/mediaupload/test/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(libs.coroutines.core) api(projects.libraries.mediaupload.api) implementation(projects.libraries.core) implementation(projects.tests.testutils) diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index c07ebb6ec9..3750060eef 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -31,6 +31,10 @@ class FakeMediaPreProcessor( var cleanUpCallCount = 0 private set + /** The [MediaOptimizationConfig] passed to the most recent [process] call, or `null` if it was never called. */ + var lastMediaOptimizationConfig: MediaOptimizationConfig? = null + private set + private var result: Result = Result.success( MediaUploadInfo.AnyFile( File("test"), @@ -51,6 +55,7 @@ class FakeMediaPreProcessor( ): Result = simulateLongTask { processLatch?.await() processCallCount++ + lastMediaOptimizationConfig = mediaOptimizationConfig result } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt index 74b479dc8c..adfffd8af4 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt @@ -17,6 +17,7 @@ import kotlinx.parcelize.Parcelize data class MediaInfo( val filename: String, val caption: String?, + val formattedCaption: CharSequence? = null, val mimeType: String, val fileSize: Long?, val formattedFileSize: String, @@ -33,6 +34,7 @@ data class MediaInfo( fun anImageMediaInfo( senderId: UserId? = UserId("@alice:server.org"), caption: String? = null, + formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -40,6 +42,7 @@ fun anImageMediaInfo( filename = "an image file.jpg", fileSize = 4 * 1024 * 1024, caption = caption, + formattedCaption = formattedCaption, mimeType = MimeTypes.Jpeg, formattedFileSize = "4MB", fileExtension = "jpg", @@ -54,6 +57,7 @@ fun anImageMediaInfo( fun aVideoMediaInfo( caption: String? = null, + formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -62,6 +66,7 @@ fun aVideoMediaInfo( filename = "a video file.mp4", fileSize = 14 * 1024 * 1024, caption = caption, + formattedCaption = formattedCaption, mimeType = MimeTypes.Mp4, formattedFileSize = "14MB", fileExtension = "mp4", @@ -77,6 +82,7 @@ fun aVideoMediaInfo( fun aPdfMediaInfo( filename: String = "a pdf file.pdf", caption: String? = null, + formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -84,6 +90,7 @@ fun aPdfMediaInfo( filename = filename, fileSize = 23 * 1024 * 1024, caption = caption, + formattedCaption = formattedCaption, mimeType = MimeTypes.Pdf, formattedFileSize = "23MB", fileExtension = "pdf", @@ -96,6 +103,30 @@ fun aPdfMediaInfo( duration = null, ) +fun aZipMediaInfo( + filename: String = "a zip file.zip", + caption: String? = null, + formattedCaption: CharSequence? = null, + senderName: String? = null, + dateSent: String? = null, + dateSentFull: String? = null, +): MediaInfo = MediaInfo( + filename = filename, + fileSize = 12 * 1024 * 1024, + caption = caption, + formattedCaption = formattedCaption, + mimeType = MimeTypes.Pdf, + formattedFileSize = "45MB", + fileExtension = "zip", + senderId = UserId("@alice:server.org"), + senderName = senderName, + senderAvatar = null, + dateSent = dateSent, + dateSentFull = dateSentFull, + waveform = null, + duration = null, +) + fun anApkMediaInfo( senderId: UserId? = UserId("@alice:server.org"), senderName: String? = null, @@ -105,6 +136,7 @@ fun anApkMediaInfo( filename = "an apk file.apk", fileSize = 50 * 1024 * 1024, caption = null, + formattedCaption = null, mimeType = MimeTypes.Apk, formattedFileSize = "50MB", fileExtension = "apk", @@ -120,6 +152,7 @@ fun anApkMediaInfo( fun anAudioMediaInfo( filename: String = "an audio file.mp3", caption: String? = null, + formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -129,6 +162,7 @@ fun anAudioMediaInfo( filename = filename, fileSize = 7 * 1024 * 1024, caption = caption, + formattedCaption = formattedCaption, mimeType = MimeTypes.Mp3, formattedFileSize = "7MB", fileExtension = "mp3", @@ -144,6 +178,7 @@ fun anAudioMediaInfo( fun aVoiceMediaInfo( filename: String = "a voice file.ogg", caption: String? = null, + formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -153,6 +188,7 @@ fun aVoiceMediaInfo( filename = filename, fileSize = 3 * 1024 * 1024, caption = caption, + formattedCaption = formattedCaption, mimeType = MimeTypes.Ogg, formattedFileSize = "3MB", fileExtension = "ogg", @@ -168,6 +204,7 @@ fun aVoiceMediaInfo( fun aTxtMediaInfo( filename: String = "a text file.txt", caption: String? = null, + formattedCaption: CharSequence? = null, senderName: String? = null, dateSent: String? = null, dateSentFull: String? = null, @@ -175,6 +212,7 @@ fun aTxtMediaInfo( filename = filename, fileSize = 2 * 1024, caption = caption, + formattedCaption = formattedCaption, mimeType = MimeTypes.PlainText, formattedFileSize = "2kB", fileExtension = "txt", diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/FileExtensionAndSizeFormatter.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/FileExtensionAndSizeFormatter.kt index fc40bca8dd..e546f50565 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/FileExtensionAndSizeFormatter.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/FileExtensionAndSizeFormatter.kt @@ -12,8 +12,8 @@ fun formatFileExtensionAndSize(extension: String, size: String?): String { return buildString { append(extension.uppercase()) if (size != null) { - append(' ') - append("($size)") + append(" • ") + append(size) } } } diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index 9c2342ecd9..b723578829 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -25,9 +25,13 @@ android { setupDependencyInjection() dependencies { + implementation(libs.matrix.richtexteditor.compose) + implementation(libs.matrix.richtexteditor) + implementation(projects.libraries.textcomposer.impl) implementation(libs.coroutines.core) implementation(libs.coil.compose) implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.exoplayer.midi) implementation(libs.androidx.media3.ui) implementation(libs.telephoto.zoomableimage) implementation(libs.vanniktech.blurhash) @@ -46,6 +50,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixmedia.api) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.uiUtils) implementation(projects.libraries.voiceplayer.api) implementation(projects.services.toolbox.api) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt index e1e112ecfa..138f6382d2 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt @@ -32,6 +32,7 @@ class DefaultMediaViewerEntryPoint : MediaViewerEntryPoint { filename = filename, fileSize = null, caption = null, + formattedCaption = null, mimeType = mimeType, formattedFileSize = "", fileExtension = "", diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt index 379c70f960..898a29aa7a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactory.kt @@ -98,6 +98,7 @@ class EventItemFactory( filename = type.filename, fileSize = type.info?.size, caption = type.caption, + formattedCaption = type.formattedCaption?.body, mimeType = type.info?.mimetype.orEmpty(), formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(type.filename), @@ -118,6 +119,7 @@ class EventItemFactory( filename = type.filename, fileSize = type.info?.size, caption = type.caption, + formattedCaption = type.formattedCaption?.body, mimeType = type.info?.mimetype.orEmpty(), formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(type.filename), @@ -139,6 +141,7 @@ class EventItemFactory( filename = type.filename, fileSize = type.info?.size, caption = type.caption, + formattedCaption = type.formattedCaption?.body, mimeType = type.info?.mimetype.orEmpty(), formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(type.filename), @@ -160,6 +163,7 @@ class EventItemFactory( filename = type.filename, fileSize = type.info?.size, caption = type.caption, + formattedCaption = type.formattedCaption?.body, mimeType = type.info?.mimetype.orEmpty(), formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(type.filename), @@ -181,6 +185,7 @@ class EventItemFactory( filename = type.filename, fileSize = type.info?.size, caption = type.caption, + formattedCaption = type.formattedCaption?.body, mimeType = type.info?.mimetype.orEmpty(), formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(type.filename), @@ -202,6 +207,7 @@ class EventItemFactory( filename = type.filename, fileSize = type.info?.size, caption = type.caption, + formattedCaption = type.formattedCaption?.body, mimeType = type.info?.mimetype.orEmpty(), formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(type.filename), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt index 722e14a790..7bdd4872a3 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -27,10 +28,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach +import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean interface MediaGalleryDataSource { - fun start() + val isReady: Boolean + fun start(coroutineScope: CoroutineScope) fun groupedMediaItemsFlow(): Flow> fun getLastData(): AsyncData suspend fun loadMore(direction: Timeline.PaginationDirection) @@ -47,7 +50,7 @@ class TimelineMediaGalleryDataSource( ) : MediaGalleryDataSource { private var timeline: Timeline? = null - private val groupedMediaItemsFlow = MutableSharedFlow>(replay = 1) + private val groupedMediaItemsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 10) override fun groupedMediaItemsFlow(): Flow> = groupedMediaItemsFlow @@ -57,9 +60,12 @@ class TimelineMediaGalleryDataSource( private val isStarted = AtomicBoolean(false) + override val isReady: Boolean get() = isStarted.get() && timeline != null + @OptIn(ExperimentalCoroutinesApi::class) - override fun start() { + override fun start(coroutineScope: CoroutineScope) { if (!isStarted.compareAndSet(false, true)) { + Timber.w("MediaGalleryDataSource for room ${room.roomId} is already started, ignoring subsequent start call") return } flow { @@ -71,10 +77,12 @@ class TimelineMediaGalleryDataSource( } mediaTimeline.getTimeline().fold( { + Timber.d("Timeline media data source flow started for room ${room.roomId}") timeline = it emit(it) }, { + Timber.e(it, "Failed to get media timeline for room ${room.roomId}") groupedMediaItemsFlow.emit(AsyncData.Failure(it)) }, ) @@ -96,19 +104,22 @@ class TimelineMediaGalleryDataSource( groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems)) } .onCompletion { - timeline?.close() + timeline?.let { + Timber.d("Timeline media gallery data source flow completed for room ${room.roomId}, closing timeline") + it.close() + } } - .launchIn(room.roomCoroutineScope) + .launchIn(coroutineScope) } override suspend fun loadMore(direction: Timeline.PaginationDirection) { - timeline?.paginate(direction) + timeline?.paginate(direction) ?: Timber.w("Timeline is not ready yet, cannot load more media items for room ${room.roomId}") } override suspend fun deleteItem(eventId: EventId) { timeline?.redactEvent( eventOrTransactionId = eventId.toEventOrTransactionId(), reason = null, - ) + ) ?: Timber.w("Timeline is not ready yet, cannot delete media item with eventId $eventId for room ${room.roomId}") } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt index 722126fac6..245e3a14b8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt @@ -16,6 +16,7 @@ sealed interface MediaBottomSheetState { data object Hidden : MediaBottomSheetState data class Details( + val fromGallery: Boolean, val eventId: EventId?, val canDelete: Boolean, val mediaInfo: MediaInfo, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt index d5fd46d507..72b16c4e1d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.mediaviewer.api.MediaInfo @@ -26,7 +27,7 @@ open class MediaBottomSheetStateDeleteConfirmationProvider : PreviewParameterPro fun aMediaBottomSheetStateDeleteConfirmation( mediaInfo: MediaInfo = anImageMediaInfo( - senderName = "Alice", + senderName = USER_NAME_ALICE, ), thumbnailSource: MediaSource? = null, ) = MediaBottomSheetState.DeleteConfirmation( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt index e79bfa5e77..7540d49b8a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.details import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo @@ -28,17 +29,22 @@ open class MediaBottomSheetStateDetailsProvider : PreviewParameterProvider @@ -148,16 +114,45 @@ fun MediaDetailsBottomSheet( ListItem( leadingContent = icon, headlineContent = { Text(wording) }, - style = ListItemStyle.Primary, onClick = { onOpenWith(state.eventId) } ) + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VisibilityOn())), + headlineContent = { Text(stringResource(CommonStrings.action_view_in_timeline)) }, + onClick = { + onViewInTimeline(state.eventId) + } + ) + if (state.fromGallery) { + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ShareAndroid())), + headlineContent = { Text(stringResource(CommonStrings.action_share)) }, + onClick = { + onShare(state.eventId) + } + ) + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())), + headlineContent = { Text(stringResource(CommonStrings.action_download)) }, + onClick = { + onDownload(state.eventId) + } + ) + } + ListItem( + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Forward())), + headlineContent = { Text(stringResource(CommonStrings.action_forward)) }, + onClick = { + onForward(state.eventId) + } + ) if (state.canDelete) { HorizontalDivider() ListItem( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())), - headlineContent = { Text(stringResource(CommonStrings.action_delete)) }, + headlineContent = { Text(stringResource(CommonStrings.action_delete_file)) }, style = ListItemStyle.Destructive, onClick = { onDelete(state.eventId) @@ -221,16 +216,14 @@ private fun SenderRow( } @Composable -private fun ColumnScope.Title() { +private fun Title() { Text( modifier = Modifier - .align(Alignment.CenterHorizontally) .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp) .semantics { heading() }, text = stringResource(R.string.screen_media_details_title), - textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index ac9b365099..25e27330b4 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -78,7 +78,7 @@ class MediaGalleryPresenter( .collectAsState(AsyncData.Uninitialized) LaunchedEffect(Unit) { - mediaGalleryDataSource.start() + mediaGalleryDataSource.start(this) } val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms -> @@ -94,7 +94,9 @@ class MediaGalleryPresenter( mode = event.mode } is MediaGalleryEvent.LoadMore -> coroutineScope.launch { - mediaGalleryDataSource.loadMore(event.direction) + if (mediaGalleryDataSource.isReady) { + mediaGalleryDataSource.loadMore(event.direction) + } } is MediaGalleryEvent.Delete -> coroutineScope.launch { mediaGalleryDataSource.deleteItem(event.eventId) @@ -127,6 +129,7 @@ class MediaGalleryPresenter( } is MediaGalleryEvent.OpenInfo -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.Details( + fromGallery = true, eventId = event.mediaItem.eventId(), canDelete = when (event.mediaItem.mediaInfo().senderId) { null -> false diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt index 7b5edf594a..75b849cd0c 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.media.WaveFormSamples +import io.element.android.libraries.designsystem.preview.ROOM_NAME import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails @@ -112,7 +113,7 @@ open class MediaGalleryStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt index c7a01ea494..071e0dba64 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt @@ -100,7 +100,11 @@ class AndroidLocalMediaActions( override suspend fun share(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) runCatchingExceptions { - val shareableUri = localMedia.toShareableUri() + // Make a copy of the shared file in the cache directory, otherwise the original file will be gone once this screen is dismissed + // and will prevent sharing the media to another room inside the app. + val copiedFile = localMedia.uri.toFile() + .copyTo(File(context.cacheDir, "temp/media/" + (localMedia.uri.lastPathSegment ?: "shared_file")), true) + val shareableUri = copiedFile.toShareableUri() val shareMediaIntent = Intent(Intent.ACTION_SEND) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .putExtra(Intent.EXTRA_STREAM, shareableUri) @@ -157,10 +161,13 @@ class AndroidLocalMediaActions( } } - private fun LocalMedia.toShareableUri(): Uri { - val mediaAsFile = this.toFile() + private fun File.toShareableUri(): Uri { val authority = "${buildMeta.applicationId}.fileprovider" - return FileProvider.getUriForFile(context, authority, mediaAsFile).normalizeScheme() + return FileProvider.getUriForFile(context, authority, this).normalizeScheme() + } + + private fun LocalMedia.toShareableUri(): Uri { + return this.toFile().toShareableUri() } @RequiresApi(Build.VERSION_CODES.Q) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt index 05cbe40f36..09164e36d1 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt @@ -9,7 +9,9 @@ package io.element.android.libraries.mediaviewer.impl.local import android.content.Context +import android.graphics.BitmapFactory import android.net.Uri +import android.webkit.MimeTypeMap import androidx.core.net.toUri import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding @@ -17,6 +19,7 @@ import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.androidutils.file.getMimeType import io.element.android.libraries.androidutils.filesize.FileSizeFormatter +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.UserId @@ -85,8 +88,12 @@ class AndroidLocalMediaFactory( waveform: List?, duration: String?, ): LocalMedia { - val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream val fileName = name ?: context.getFileName(uri) ?: "" + val resolvedMimeType = resolveMimeType( + uri = uri, + mimeType = mimeType, + fileName = fileName, + ) val fileSize = context.getFileSize(uri) val calculatedFormattedFileSize = formattedFileSize ?: fileSizeFormatter.format(fileSize) val fileExtension = fileExtensionExtractor.extractFromName(fileName) @@ -97,6 +104,7 @@ class AndroidLocalMediaFactory( filename = fileName, fileSize = fileSize, caption = caption, + formattedCaption = null, formattedFileSize = calculatedFormattedFileSize, fileExtension = fileExtension, senderId = senderId, @@ -109,4 +117,36 @@ class AndroidLocalMediaFactory( ) ) } + + private fun resolveMimeType( + uri: Uri, + mimeType: String?, + fileName: String, + ): String { + val explicitMimeType = mimeType.takeUnless { it.isNullOrBlank() || it == MimeTypes.OctetStream } + if (explicitMimeType != null) return explicitMimeType + + val resolverMimeType = context.getMimeType(uri).takeUnless { it.isNullOrBlank() || it == MimeTypes.OctetStream } + if (resolverMimeType != null) return resolverMimeType + + val decodedImageMimeType = decodeImageMimeType(uri) + if (decodedImageMimeType != null) return decodedImageMimeType + + val extensionMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + fileExtensionExtractor.extractFromName(fileName) + ) + if (!extensionMimeType.isNullOrBlank()) return extensionMimeType + + return MimeTypes.OctetStream + } + + private fun decodeImageMimeType(uri: Uri): String? { + return tryOrNull { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeStream(inputStream, null, options) + options.outMimeType + } + } + } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt index 96450d99bf..319aebe33e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt @@ -41,6 +41,7 @@ class DefaultLocalMediaRenderer( textFileViewer = textFileViewer, audioFocus = audioFocus, onClick = {}, + onOpenWith = null, ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt index cc276b52c8..c19b1a24c0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt @@ -31,6 +31,7 @@ fun LocalMediaView( bottomPaddingInPixels: Int, audioFocus: AudioFocus?, onClick: () -> Unit, + onOpenWith: (() -> Unit)?, textFileViewer: TextFileViewer, modifier: Modifier = Modifier, isDisplayed: Boolean = true, @@ -80,7 +81,7 @@ fun LocalMediaView( uri = localMedia?.uri, info = mediaInfo, modifier = modifier, - onClick = onClick, + onOpenWith = onOpenWith, ) } } 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 cb9d6ae9c8..3ac578196d 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 @@ -88,7 +88,7 @@ fun MediaAudioView( modifier: Modifier = Modifier, isDisplayed: Boolean = true, ) { - val exoPlayer = rememberExoPlayer() + val exoPlayer = rememberExoPlayer(forAudioOnly = true) ExoPlayerMediaAudioView( isDisplayed = isDisplayed, localMediaViewState = localMediaViewState, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt index 912ea88e99..a1c57ef272 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt @@ -9,23 +9,16 @@ package io.element.android.libraries.mediaviewer.impl.local.file import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource 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.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter @@ -33,55 +26,54 @@ 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.core.bool.orFalse +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio +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.Icon +import io.element.android.libraries.designsystem.theme.components.ButtonSize +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize +import io.element.android.libraries.mediaviewer.impl.R 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 +/** + * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623 + */ @Composable fun MediaFileView( localMediaViewState: LocalMediaViewState, uri: Uri?, info: MediaInfo?, - onClick: () -> Unit, + onOpenWith: (() -> Unit)?, modifier: Modifier = Modifier, ) { val isAudio = info?.mimeType.isMimeTypeAudio().orFalse() localMediaViewState.isReady = uri != null - - val interactionSource = remember { MutableInteractionSource() } Box( modifier = modifier - .padding(horizontal = 8.dp) - .clickable( - onClick = onClick, - interactionSource = interactionSource, - indication = null - ), + .padding(horizontal = 8.dp), contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Box( - modifier = Modifier - .size(72.dp) - .clip(CircleShape) - .background(ElementTheme.colors.iconPrimary), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = if (isAudio) CompoundIcons.Audio() else CompoundIcons.Attachment(), - contentDescription = null, - tint = ElementTheme.colors.iconOnSolidPrimary, - modifier = Modifier - .size(32.dp) - .rotate(if (isAudio) 0f else -45f), - ) + val isApk = info?.mimeType == MimeTypes.Apk + val icon = when { + isAudio -> CompoundIcons.Audio() + isApk -> CompoundIcons.Bug() + else -> CompoundIcons.Files() } + BigIcon( + modifier = Modifier.align(Alignment.CenterHorizontally), + style = BigIcon.Style.Default( + vectorIcon = icon, + usePrimaryTint = true, + ), + ) if (info != null) { Spacer(modifier = Modifier.height(20.dp)) Text( @@ -100,6 +92,20 @@ fun MediaFileView( overflow = TextOverflow.Ellipsis, color = ElementTheme.colors.textPrimary ) + if (onOpenWith != null) { + val (icon, textResId) = if (isApk) { + IconSource.Resource(R.drawable.ic_apk_install) to CommonStrings.common_install_apk_android + } else { + IconSource.Vector(CompoundIcons.PopOut()) to CommonStrings.action_open_with + } + OutlinedButton( + modifier = Modifier.padding(top = 24.dp), + size = ButtonSize.Small, + leadingIcon = icon, + onClick = onOpenWith, + text = stringResource(id = textResId), + ) + } } } } @@ -115,6 +121,6 @@ internal fun MediaFileViewPreview( localMediaViewState = rememberLocalMediaViewState(), uri = null, info = info, - onClick = {}, + onOpenWith = {}, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt index 8e41a4809a..3214236253 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt @@ -10,11 +10,15 @@ package io.element.android.libraries.mediaviewer.impl.local.file import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.mediaviewer.api.MediaInfo -import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo +import io.element.android.libraries.mediaviewer.api.aZipMediaInfo +import io.element.android.libraries.mediaviewer.api.anApkMediaInfo +import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo open class MediaInfoFileProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aPdfMediaInfo(), + aZipMediaInfo(), + anAudioMediaInfo(), + anApkMediaInfo(), ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt index d0043c657a..ccd628dbc5 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt @@ -8,14 +8,18 @@ package io.element.android.libraries.mediaviewer.impl.local.player +import androidx.annotation.OptIn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer +@OptIn(UnstableApi::class) @Composable -fun rememberExoPlayer(): ExoPlayer { +fun rememberExoPlayer(forAudioOnly: Boolean): ExoPlayer { return if (LocalInspectionMode.current) { remember { ExoPlayerForPreview() @@ -23,7 +27,14 @@ fun rememberExoPlayer(): ExoPlayer { } else { val context = LocalContext.current remember { - ExoPlayer.Builder(context).build() + if (forAudioOnly) { + // Required for media3-exoplayer-midi to decode MIDI samples produced by DefaultExtractorsFactory. + val renderersFactory = DefaultRenderersFactory(context) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + ExoPlayer.Builder(context, renderersFactory).build() + } else { + ExoPlayer.Builder(context).build() + } } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt index 11f99a3ca8..4da8433cd1 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt @@ -258,7 +258,7 @@ class ExoPlayerForPreview( override fun setPriorityTaskManager(priorityTaskManager: PriorityTaskManager?) {} override fun isSleepingForOffload(): Boolean = throw NotImplementedError() override fun isTunnelingEnabled(): Boolean = throw NotImplementedError() - override fun isReleased(): Boolean = throw NotImplementedError() + override fun isReleased(): Boolean = false override fun setImageOutput(imageOutput: ImageOutput?) {} override fun setAudioCodecParameters(codecParameters: CodecParameters) {} override fun addAudioCodecParametersChangeListener(listener: CodecParametersChangeListener, keys: List) {} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt index b83c598c10..1f2b0b93d2 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt @@ -12,13 +12,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -28,8 +27,10 @@ import androidx.compose.runtime.rememberUpdatedState 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.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -91,49 +92,65 @@ fun MediaPlayerControllerView( .widthIn(max = 480.dp), verticalAlignment = Alignment.CenterVertically, ) { - val bgColor = if (state.isPlaying) { - ElementTheme.colors.bgCanvasDefault + val colors = if (state.isPlaying) { + IconButtonDefaults.iconButtonColors( + containerColor = ElementTheme.colors.bgCanvasDefault, + contentColor = ElementTheme.colors.iconPrimary, + ) } else { - ElementTheme.colors.textPrimary + IconButtonDefaults.iconButtonColors( + containerColor = ElementTheme.colors.iconPrimary, + contentColor = ElementTheme.colors.iconOnSolidPrimary, + ) } - Box( + val a11yPause = stringResource(CommonStrings.a11y_pause) + val a11yPlay = stringResource(CommonStrings.a11y_play) + IconButton( modifier = Modifier .size(36.dp) - .background( - color = bgColor, - shape = CircleShape, - ) - .clip(CircleShape) - .clickable { onTogglePlay() } - .padding(8.dp), - contentAlignment = Alignment.Center, + .semantics { + stateDescription = if (state.isPlaying) a11yPause else a11yPlay + }, + onClick = onTogglePlay, + colors = colors, ) { if (state.isPlaying) { Icon( imageVector = CompoundIcons.PauseSolid(), - tint = ElementTheme.colors.iconPrimary, - contentDescription = stringResource(CommonStrings.a11y_pause) + contentDescription = null, ) } else { Icon( imageVector = CompoundIcons.PlaySolid(), - tint = ElementTheme.colors.iconOnSolidPrimary, - contentDescription = stringResource(CommonStrings.a11y_play) + contentDescription = null, ) } } + val position = state.displayProgressInMillis.toHumanReadableDuration() + val a11yPosition = stringResource(CommonStrings.a11y_position, position) Text( modifier = Modifier .widthIn(min = 48.dp) - .padding(horizontal = 8.dp), - text = state.displayProgressInMillis.toHumanReadableDuration(), + .padding(horizontal = 8.dp) + .semantics { + contentDescription = a11yPosition + }, + text = position, textAlign = TextAlign.Center, color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyXsMedium, ) var lastSelectedValue by remember { mutableFloatStateOf(-1f) } Slider( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .semantics { + // Speak out a progress percent instead of milliseconds + stateDescription = buildString { + append((state.progressAsFloat * 100).toInt()) + append("%") + } + }, valueRange = 0f..state.durationInMillis.toFloat(), value = lastSelectedValue.takeIf { it >= 0 } ?: state.seekingToMillis?.toFloat() @@ -150,30 +167,40 @@ fun MediaPlayerControllerView( val formattedDuration = remember(state.durationInMillis) { state.durationInMillis.toHumanReadableDuration() } + val a11yDuration = stringResource(CommonStrings.a11y_duration, formattedDuration) Text( modifier = Modifier .widthIn(min = 48.dp) - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .semantics { + contentDescription = a11yDuration + }, text = formattedDuration, textAlign = TextAlign.Center, color = ElementTheme.colors.textPrimary, style = ElementTheme.typography.fontBodyXsMedium, ) if (state.canMute) { + val a11yUnmute = stringResource(CommonStrings.common_unmute) + val a11yMute = stringResource(CommonStrings.common_mute) IconButton( onClick = onToggleMute, + modifier = Modifier + .semantics { + stateDescription = if (state.isMuted) a11yUnmute else a11yMute + }, ) { if (state.isMuted) { Icon( imageVector = CompoundIcons.VolumeOffSolid(), tint = ElementTheme.colors.iconPrimary, - contentDescription = stringResource(CommonStrings.common_unmute) + contentDescription = null, ) } else { Icon( imageVector = CompoundIcons.VolumeOnSolid(), tint = ElementTheme.colors.iconPrimary, - contentDescription = stringResource(CommonStrings.common_mute) + contentDescription = null, ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt index 082dc0571c..21279a598e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt @@ -33,10 +33,13 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem +import androidx.media3.common.ParserException +import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.Player.STATE_READY import androidx.media3.common.Timeline import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView @@ -57,6 +60,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPla import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState +import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import kotlinx.coroutines.delay import me.saket.telephoto.zoomable.zoomable import timber.log.Timber @@ -73,7 +77,7 @@ fun MediaVideoView( audioFocus: AudioFocus?, modifier: Modifier = Modifier, ) { - val exoPlayer = rememberExoPlayer() + val exoPlayer = rememberExoPlayer(forAudioOnly = false) ExoPlayerMediaVideoView( isDisplayed = isDisplayed, localMediaViewState = localMediaViewState, @@ -157,17 +161,38 @@ private fun ExoPlayerMediaVideoView( isReady = playbackState == STATE_READY, ) } + + override fun onPlayerError(error: PlaybackException) { + super.onPlayerError(error) + Timber.w(error, "Player error") + if (error is ExoPlaybackException && error.cause is ParserException) { + // The cause can be: + // androidx.media3.common.ParserException: Invalid NAL length {contentIsMalformed=true, dataType=1} + // This has been observed when the user wants to play a second time a recorded video. + // Workaround the issue #6956, start the playback again + exoPlayer.prepare() + exoPlayer.play() + } + } } } var autoHideController by remember { mutableIntStateOf(0) } - LaunchedEffect(autoHideController) { - delay(5.seconds) - if (exoPlayer.isPlaying) { + val isTalkbackActive = isTalkbackActive() + LaunchedEffect(autoHideController, isTalkbackActive) { + if (isTalkbackActive) { + // Ensure that the controller is always visible when talkback is active mediaPlayerControllerState = mediaPlayerControllerState.copy( - isVisible = false, + isVisible = true, ) + } else { + delay(5.seconds) + if (exoPlayer.isPlaying) { + mediaPlayerControllerState = mediaPlayerControllerState.copy( + isVisible = false, + ) + } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index a9fb5d645c..6feb3c7d53 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -11,14 +11,18 @@ package io.element.android.libraries.mediaviewer.impl.viewer import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.ProduceStateScope import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberUpdatedState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.extensions.mapCatchingExceptions +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.local.LocalMedia @@ -35,13 +39,18 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap class MediaViewerDataSource( mode: MediaViewerMode, + coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val galleryDataSource: MediaGalleryDataSource, private val mediaLoader: MatrixMediaLoader, @@ -50,7 +59,7 @@ class MediaViewerDataSource( private val pagerKeysHandler: PagerKeysHandler, ) { // List of media files that are currently being loaded - private val mediaFiles: MutableList = mutableListOf() + private val mediaFiles: ConcurrentHashMap = ConcurrentHashMap() private val galleryMode = when (mode) { MediaViewerMode.SingleMedia, @@ -62,50 +71,70 @@ class MediaViewerDataSource( private val localMediaStates: MutableMap>> = mutableMapOf() - fun setup() { - galleryDataSource.start() + fun setup(coroutineScope: CoroutineScope) { + galleryDataSource.start(coroutineScope) } fun dispose() { - mediaFiles.forEach { it.close() } + Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files") + mediaFiles.values.forEach { it.close() } mediaFiles.clear() localMediaStates.clear() } + /** + * Helper function to translate the [dataFlow] result to a Compose [State] that can be observed in the UI. + */ @Composable - fun collectAsState(): State> { - return remember { dataFlow() }.collectAsState(initialData()) + fun produceState( + producer: suspend ProduceStateScope>.(StateFlow>) -> Unit + ): State> { + val latestProducer by rememberUpdatedState(producer) + return produceState(initialValue = initialData()) { + latestProducer(dataFlow) + } + } + + /** + * Find the index of the page corresponding to the given eventId, or null if not found. + */ + fun findEventIndex(eventId: EventId?): Int? { + if (eventId == null) return null + return dataFlow.value.indexOfFirst { (it as? MediaViewerPageData.MediaViewerData)?.eventId == eventId }.takeIf { it >= 0 } } @VisibleForTesting - internal fun dataFlow(): Flow> { - return galleryDataSource.groupedMediaItemsFlow() - .map { groupedItems -> - when (groupedItems) { - AsyncData.Uninitialized, - is AsyncData.Loading -> { - persistentListOf( - MediaViewerPageData.Loading( - direction = Timeline.PaginationDirection.BACKWARDS, - timestamp = systemClock.epochMillis(), - pagerKey = Long.MIN_VALUE, - ) + internal val dataFlow: StateFlow> = galleryDataSource.groupedMediaItemsFlow() + .map { groupedItems -> + when (groupedItems) { + AsyncData.Uninitialized, + is AsyncData.Loading -> { + persistentListOf( + MediaViewerPageData.Loading( + direction = Timeline.PaginationDirection.BACKWARDS, + timestamp = systemClock.epochMillis(), + pagerKey = Long.MIN_VALUE, ) - } - is AsyncData.Failure -> { - persistentListOf( - MediaViewerPageData.Failure(groupedItems.error), - ) - } - is AsyncData.Success -> { - withContext(dispatcher) { - val mediaItems = groupedItems.data.getItems(galleryMode) - buildMediaViewerPageList(mediaItems) - } + ) + } + is AsyncData.Failure -> { + persistentListOf( + MediaViewerPageData.Failure(groupedItems.error), + ) + } + is AsyncData.Success -> { + withContext(dispatcher) { + val mediaItems = groupedItems.data.getItems(galleryMode) + buildMediaViewerPageList(mediaItems) } } } - } + } + .stateIn( + scope = CoroutineScope(coroutineScope.coroutineContext + dispatcher), + started = SharingStarted.Lazily, + initialValue = initialData(), + ) private fun initialData(): ImmutableList { val initialMediaItems = @@ -120,25 +149,11 @@ class MediaViewerDataSource( */ private fun buildMediaViewerPageList(groupedItems: List) = buildList { // Filter out DateSeparator items, we do not need them for the media viewer - val itemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator } - // Separate loading indicators and media events - val loadingIndicators = itemsNoDateSeparator.filterIsInstance() - val mediaEvents = itemsNoDateSeparator.filterIsInstance() - // Determine backward and forward loading indicators - val backwardLoading = loadingIndicators.find { it.direction == Timeline.PaginationDirection.BACKWARDS } - val forwardLoading = loadingIndicators.find { it.direction == Timeline.PaginationDirection.FORWARDS } - // Build ordered list: backward loading, media events (oldest first), forward loading - // Media events are currently newest first, reverse to get oldest first - val orderedEvents = mediaEvents.reversed() - // Create new list of MediaItem in order: backwardLoading, orderedEvents, forwardLoading - val orderedItems = buildList { - backwardLoading?.let { add(it) } - addAll(orderedEvents) - forwardLoading?.let { add(it) } - } - pagerKeysHandler.accept(orderedItems) - orderedItems.forEach { mediaItem -> + val groupedItemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator } + pagerKeysHandler.accept(groupedItemsNoDateSeparator) + groupedItemsNoDateSeparator.forEach { mediaItem -> when (mediaItem) { + is MediaItem.DateSeparator -> Unit is MediaItem.Event -> { val sourceUrl = mediaItem.mediaSource().safeUrl val localMedia = localMediaStates.getOrPut(sourceUrl) { @@ -158,11 +173,10 @@ class MediaViewerDataSource( is MediaItem.LoadingIndicator -> add( MediaViewerPageData.Loading( direction = mediaItem.direction, - timestamp = systemClock.epochMillis(), + timestamp = mediaItem.timestamp, pagerKey = pagerKeysHandler.getKey(mediaItem), ) ) - is MediaItem.DateSeparator -> Unit // already filtered out } } }.toImmutableList() @@ -172,10 +186,18 @@ class MediaViewerDataSource( } suspend fun loadMore(direction: Timeline.PaginationDirection) { - galleryDataSource.loadMore(direction) + if (galleryDataSource.isReady) { + galleryDataSource.loadMore(direction) + } } suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) { + val currentState = localMediaStates[data.mediaSource.safeUrl]?.value + // If the media is already loading or has been loaded successfully, do nothing + if (currentState?.isLoading() == true || currentState?.isSuccess() == true) { + return + } + Timber.d("loadMedia for ${data.eventId}") val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) { mutableStateOf(AsyncData.Uninitialized) @@ -188,7 +210,7 @@ class MediaViewerDataSource( filename = data.mediaInfo.filename ) .onSuccess { mediaFile -> - mediaFiles.add(mediaFile) + mediaFiles[data.mediaSource] = mediaFile } .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( @@ -203,4 +225,12 @@ class MediaViewerDataSource( localMediaState.value = AsyncData.Failure(it) } } + + fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) { + if (localMediaStates[data.mediaSource.safeUrl]?.value?.isLoading() == true) { + Timber.d("cancelLoadingMedia for ${data.eventId}") + mediaFiles.remove(data.mediaSource)?.close() + localMediaStates[data.mediaSource.safeUrl]?.value = AsyncData.Uninitialized + } + } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt index 1960b69e4c..53d287075d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt @@ -29,4 +29,5 @@ sealed interface MediaViewerEvent { data class Delete(val eventId: EventId) : MediaViewerEvent data class OnNavigateTo(val index: Int) : MediaViewerEvent data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent + data class CancelLoadingMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index d834534e3e..e594daeb41 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -118,6 +119,7 @@ class MediaViewerNode( navigator = this, dataSource = MediaViewerDataSource( mode = inputs.mode, + coroutineScope = lifecycleScope, dispatcher = coroutineDispatchers.computation, galleryDataSource = mediaGallerySource, mediaLoader = mediaLoader, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index b7631a7039..eec99468f7 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -14,14 +14,12 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.IntState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject @@ -45,10 +43,7 @@ import io.element.android.libraries.mediaviewer.impl.model.mediaPermissions import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import io.element.android.libraries.androidutils.R as UtilsR @@ -75,12 +70,27 @@ class MediaViewerPresenter( @Composable override fun present(): MediaViewerState { val coroutineScope = rememberCoroutineScope() - val data = dataSource.collectAsState() - val currentIndex = remember { mutableIntStateOf(searchIndex(data.value, inputs.eventId)) } + val currentIndex = remember { mutableIntStateOf(dataSource.findEventIndex(inputs.eventId) ?: 0) } + val data = dataSource.produceState { flow -> + flow.collectLatest { new -> + val existingItem = value.getOrNull(currentIndex.intValue) + val newItem = new.getOrNull(currentIndex.intValue) + if (existingItem is MediaViewerPageData.MediaViewerData && existingItem.eventId == inputs.eventId && newItem != existingItem) { + currentIndex.intValue = dataSource.findEventIndex(inputs.eventId) ?: 0 + } else if (currentIndex.intValue > 0 && value.firstOrNull() is MediaViewerPageData.Loading && + new.firstOrNull() !is MediaViewerPageData.Loading) { + // Restore index based on the eventId after the initial items have been loaded + currentIndex.intValue = dataSource.findEventIndex(inputs.eventId) ?: 0 + } + value = new + } + } + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - NoMoreItemsBackwardSnackBarDisplayer(currentIndex, data) - NoMoreItemsForwardSnackBarDisplayer(currentIndex, data) + // Add both forward and backward pagination state checks to display a snackbar when there is no more items to load in either direction + NoMoreItemsSnackBarDisplayer(currentIndex, data, Timeline.PaginationDirection.FORWARDS) + NoMoreItemsSnackBarDisplayer(currentIndex, data, Timeline.PaginationDirection.BACKWARDS) val permissions by room.permissionsAsState(MediaPermissions.DEFAULT) { perms -> perms.mediaPermissions() @@ -88,7 +98,7 @@ class MediaViewerPresenter( var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) } DisposableEffect(Unit) { - dataSource.setup() + dataSource.setup(coroutineScope) onDispose { dataSource.dispose() } @@ -100,6 +110,9 @@ class MediaViewerPresenter( is MediaViewerEvent.LoadMedia -> { coroutineScope.downloadMedia(data = event.data) } + is MediaViewerEvent.CancelLoadingMedia -> { + dataSource.cancelLoadingMedia(event.data) + } is MediaViewerEvent.ClearLoadingError -> { dataSource.clearLoadingError(event.data) } @@ -132,6 +145,7 @@ class MediaViewerPresenter( } is MediaViewerEvent.OpenInfo -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.Details( + fromGallery = false, eventId = event.data.eventId, canDelete = when (event.data.mediaInfo.senderId) { null -> false @@ -173,58 +187,39 @@ class MediaViewerPresenter( } @Composable - private fun NoMoreItemsBackwardSnackBarDisplayer( + private fun NoMoreItemsSnackBarDisplayer( currentIndex: IntState, data: State>, + direction: Timeline.PaginationDirection, ) { - val isRenderingLoadingBackward by remember { - derivedStateOf { - currentIndex.intValue == 0 && - data.value.size > 1 && - data.value.firstOrNull() is MediaViewerPageData.Loading && - (data.value.firstOrNull() as? MediaViewerPageData.Loading)?.direction == Timeline.PaginationDirection.BACKWARDS - } - } - if (isRenderingLoadingBackward) { - LaunchedEffect(Unit) { - // Observe the loading data vanishing - snapshotFlow { - val first = data.value.firstOrNull() - first is MediaViewerPageData.Loading && first.direction == Timeline.PaginationDirection.BACKWARDS + var previousIndex by remember { mutableIntStateOf(currentIndex.intValue) } + var previousDataSize by remember { mutableIntStateOf(data.value.size) } + var wasLoading: Boolean? by remember { mutableStateOf(null) } + LaunchedEffect(currentIndex.intValue, data.value) { + fun isLoading(index: Int, data: List, direction: Timeline.PaginationDirection): Boolean { + return when (direction) { + Timeline.PaginationDirection.BACKWARDS -> index == data.lastIndex && data.lastOrNull() is MediaViewerPageData.Loading + Timeline.PaginationDirection.FORWARDS -> index == 0 && data.firstOrNull() is MediaViewerPageData.Loading } - .distinctUntilChanged() - .filter { !it } - .onEach { showNoMoreItemsSnackbar() } - .launchIn(this) } - } - } + // Reset the effect when the user navigate to another item so we only take into account index changes caused by data changes + if (previousIndex != currentIndex.intValue) { + wasLoading = null + previousIndex = currentIndex.intValue + } + // If we were navigating backwards and the data size grew, we can discard the previous value: it means we received new items + if (direction == Timeline.PaginationDirection.BACKWARDS && previousDataSize < data.value.size) { + wasLoading = null + } - @Composable - private fun NoMoreItemsForwardSnackBarDisplayer( - currentIndex: IntState, - data: State>, - ) { - val isRenderingLoadingForward by remember { - derivedStateOf { - currentIndex.intValue == data.value.lastIndex && - data.value.size > 1 && - data.value.lastOrNull() is MediaViewerPageData.Loading && - (data.value.lastOrNull() as? MediaViewerPageData.Loading)?.direction == Timeline.PaginationDirection.FORWARDS - } - } - if (isRenderingLoadingForward) { - LaunchedEffect(Unit) { - // Observe the loading data vanishing - snapshotFlow { - val last = data.value.lastOrNull() - last is MediaViewerPageData.Loading && last.direction == Timeline.PaginationDirection.FORWARDS - } - .distinctUntilChanged() - .filter { !it } - .onEach { showNoMoreItemsSnackbar() } - .launchIn(this) + val isLoading = isLoading(currentIndex.intValue, data.value, direction) + + if (wasLoading == true && !isLoading) { + showNoMoreItemsSnackbar() } + + previousDataSize = data.value.size + wasLoading = isLoading } } @@ -296,13 +291,4 @@ class MediaViewerPresenter( CommonStrings.error_unknown } } - - private fun searchIndex(data: List, eventId: EventId?): Int { - if (eventId == null) { - return 0 - } - return data.indexOfFirst { - (it as? MediaViewerPageData.MediaViewerData)?.eventId == eventId - }.coerceAtLeast(0) - } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt index 8a4da2aac3..a51d3c44e9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer +import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -29,6 +30,7 @@ data class MediaViewerState( val eventSink: (MediaViewerEvent) -> Unit, ) +@Immutable sealed interface MediaViewerPageData { val pagerKey: Long 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 ef9b533586..8cc14d6445 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 @@ -13,6 +13,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.media.WaveFormSamples +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline @@ -179,7 +180,7 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) ), anImageMediaInfo( - senderName = "Alice", + senderName = USER_NAME_ALICE, dateSent = "21 NOV, 2024", caption = LONG_CAPTION, ).let { @@ -194,6 +195,86 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) ) }, + anImageMediaInfo( + senderName = "Bob", + dateSent = "22 NOV, 2024", + formattedCaption = "This is a bold caption", + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, it) + ), + mediaInfo = it, + ) + ) + ) + }, + anImageMediaInfo( + senderName = "Charlie", + dateSent = "23 NOV, 2024", + formattedCaption = "This is an italic caption", + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, it) + ), + mediaInfo = it, + ) + ) + ) + }, + anImageMediaInfo( + senderName = "Diana", + dateSent = "24 NOV, 2024", + formattedCaption = "This is a code caption", + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, it) + ), + mediaInfo = it, + ) + ) + ) + }, + anImageMediaInfo( + senderName = "Eve", + dateSent = "25 NOV, 2024", + formattedCaption = "
This is a quote caption
", + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Success( + LocalMedia(Uri.EMPTY, it) + ), + mediaInfo = it, + ) + ) + ) + }, + anImageMediaInfo( + senderName = "Frank", + dateSent = "26 NOV, 2024", + formattedCaption = "This caption has bold, italic, and code formatting.", + ).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 abea2f66d2..c4d442338a 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 @@ -31,8 +31,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -41,7 +44,6 @@ 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.draw.alpha @@ -49,9 +51,12 @@ 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.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow @@ -59,10 +64,12 @@ 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 +import androidx.core.text.toSpannable import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.viewfolder.api.TextFileViewer +import io.element.android.libraries.androidutils.text.safeLinkify import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo @@ -91,13 +98,15 @@ import io.element.android.libraries.mediaviewer.impl.local.LocalMediaView import io.element.android.libraries.mediaviewer.impl.local.PlayableState import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState import io.element.android.libraries.mediaviewer.impl.util.bgCanvasWithTransparency +import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.wysiwyg.compose.EditorStyledText import kotlinx.coroutines.delay import me.saket.telephoto.zoomable.OverzoomEffect import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState -val topAppBarHeight = 88.dp +val topAppBarHeight = 112.dp /** * Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=3361-16623 @@ -120,94 +129,11 @@ fun MediaViewerView( Scaffold( modifier, containerColor = Color.Transparent, - snackbarHost = { SnackbarHost(snackbarHostState) }, - ) { - val pagerState = rememberPagerState(state.currentIndex, 0f) { - state.listData.size - } - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.currentPage }.collect { page -> - state.eventSink(MediaViewerEvent.OnNavigateTo(page)) - } - } - HorizontalPager( - state = pagerState, - modifier = Modifier, - // Pre-load previous and next pages - beyondViewportPageCount = 1, - key = { index -> state.listData[index].pagerKey }, - ) { page -> - when (val dataForPage = state.listData[page]) { - is MediaViewerPageData.Failure -> { - MediaViewerErrorPage( - throwable = dataForPage.throwable, - onDismiss = onBackClick, - ) - } - is MediaViewerPageData.Loading -> { - LaunchedEffect(dataForPage.timestamp) { - state.eventSink(MediaViewerEvent.LoadMore(dataForPage.direction)) - } - MediaViewerLoadingPage( - onDismiss = onBackClick, - ) - } - is MediaViewerPageData.MediaViewerData -> { - var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } - LaunchedEffect(Unit) { - state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) - } - Box( - modifier = Modifier.fillMaxSize() - ) { - val isDisplayed = remember(pagerState.settledPage) { - // This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value - // 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 - navigationBarPadding).coerceAtLeast(0), - data = dataForPage, - textFileViewer = textFileViewer, - onDismiss = onBackClick, - onRetry = { - state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) - }, - onDismissError = { - state.eventSink(MediaViewerEvent.ClearLoadingError(dataForPage)) - }, - onShowOverlayChange = { - showOverlay = it - }, - audioFocus = audioFocus, - isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId, - ) - // Bottom bar - AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { - Box( - modifier = Modifier.fillMaxSize() - ) { - MediaViewerBottomBar( - modifier = Modifier.align(Alignment.BottomCenter), - showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(), - caption = dataForPage.mediaInfo.caption, - onHeightChange = { bottomPaddingInPixels = it }, - ) - } - } - } - } - } - } - // Top bar - AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { - Box( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + topBar = { + AnimatedVisibility( + visible = showOverlay, + enter = fadeIn(), + exit = fadeOut(), ) { when (currentData) { is MediaViewerPageData.MediaViewerData -> { @@ -248,6 +174,100 @@ fun MediaViewerView( } } } + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { + val pagerState = rememberPagerState(state.currentIndex, 0f) { + state.listData.size + } + + LaunchedEffect(pagerState.targetPage, state.currentIndex) { + // Only emit an index navigation change when it's triggered by the user scrolling + if (pagerState.targetPage != state.currentIndex && pagerState.isScrollInProgress) { + state.eventSink(MediaViewerEvent.OnNavigateTo(pagerState.targetPage)) + } + } + HorizontalPager( + state = pagerState, + modifier = Modifier, + // Pre-load previous and next pages + beyondViewportPageCount = 1, + key = { index -> state.listData[index].pagerKey }, + reverseLayout = true, + ) { page -> + when (val dataForPage = state.listData[page]) { + is MediaViewerPageData.Failure -> { + MediaViewerErrorPage( + throwable = dataForPage.throwable, + onDismiss = onBackClick, + ) + } + is MediaViewerPageData.Loading -> { + LaunchedEffect(dataForPage.timestamp) { + state.eventSink(MediaViewerEvent.LoadMore(dataForPage.direction)) + } + MediaViewerLoadingPage( + onDismiss = onBackClick, + ) + } + is MediaViewerPageData.MediaViewerData -> { + var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } + Box( + modifier = Modifier + .onVisibilityChanged(minDurationMs = 200L) { isVisible -> + if (isVisible) { + state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) + } else { + state.eventSink(MediaViewerEvent.CancelLoadingMedia(dataForPage)) + } + } + .fillMaxSize() + ) { + val isDisplayed = remember(pagerState.settledPage) { + // This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value + // 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 - navigationBarPadding).coerceAtLeast(0), + data = dataForPage, + textFileViewer = textFileViewer, + onDismiss = onBackClick, + onRetry = { + state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) + }, + onOpenWith = { + state.eventSink(MediaViewerEvent.OpenWith(dataForPage)) + }, + onDismissError = { + state.eventSink(MediaViewerEvent.ClearLoadingError(dataForPage)) + }, + onShowOverlayChange = { + showOverlay = it + }, + audioFocus = audioFocus, + isUserSelected = (state.listData[page] as? MediaViewerPageData.MediaViewerData)?.eventId == state.initiallySelectedEventId, + ) + // Bottom bar + AnimatedVisibility( + visible = showOverlay, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter), + ) { + MediaViewerBottomBar( + showDivider = dataForPage.mediaInfo.mimeType.isMimeTypeVideo(), + caption = dataForPage.mediaInfo.caption, + formattedCaption = dataForPage.mediaInfo.formattedCaption, + onHeightChange = { bottomPaddingInPixels = it }, + ) + } + } + } + } } } @@ -260,6 +280,7 @@ fun MediaViewerView( state.eventSink(MediaViewerEvent.ViewInTimeline(it)) }, onShare = { + // Note: share action is not rendered when the bottom sheet is opened from the media viewer (currentData as? MediaViewerPageData.MediaViewerData)?.let { state.eventSink(MediaViewerEvent.Share(currentData)) } @@ -268,6 +289,7 @@ fun MediaViewerView( state.eventSink(MediaViewerEvent.Forward(it)) }, onDownload = { + // Note: download action is not rendered when the bottom sheet is opened from the media viewer (currentData as? MediaViewerPageData.MediaViewerData)?.let { state.eventSink(MediaViewerEvent.SaveOnDisk(currentData)) } @@ -316,6 +338,7 @@ private fun MediaViewerPage( isUserSelected: Boolean, onDismiss: () -> Unit, onRetry: () -> Unit, + onOpenWith: () -> Unit, onDismissError: () -> Unit, onShowOverlayChange: (Boolean) -> Unit, audioFocus: AudioFocus?, @@ -370,14 +393,16 @@ private fun MediaViewerPage( currentOnShowOverlayChange(!currentShowOverlay) } }, + onOpenWith = onOpenWith, isUserSelected = isUserSelected, audioFocus = audioFocus, ) - ThumbnailView( - mediaInfo = data.mediaInfo, - thumbnailSource = data.thumbnailSource, - isVisible = showThumbnail, - ) + if (showThumbnail) { + ThumbnailView( + mediaInfo = data.mediaInfo, + thumbnailSource = data.thumbnailSource, + ) + } if (showError) { ErrorView( errorMessage = stringResource(id = CommonStrings.error_unknown), @@ -479,14 +504,20 @@ private fun MediaViewerTopBar( TopAppBar( title = { if (senderName != null && dateSent != null) { + val description = stringResource( + CommonStrings.a11y_sent_by_sender_at_date, + senderName, + dateSent, + ) Column( modifier = Modifier .fillMaxWidth() + .clearAndSetSemantics { + heading() + contentDescription = description + }, ) { Text( - modifier = Modifier.semantics { - heading() - }, text = senderName, style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textPrimary, @@ -544,6 +575,7 @@ private fun MediaViewerTopBar( @Composable private fun MediaViewerBottomBar( caption: String?, + formattedCaption: CharSequence?, showDivider: Boolean, onHeightChange: (Int) -> Unit, modifier: Modifier = Modifier, @@ -556,7 +588,7 @@ private fun MediaViewerBottomBar( onHeightChange(it.height) }, ) { - if (caption != null) { + if (caption != null || formattedCaption != null) { if (showDivider) { HorizontalDivider() } @@ -567,15 +599,28 @@ private fun MediaViewerBottomBar( .fillMaxWidth() .heightIn(max = if (hasCompactHeightWindowSize()) maxCaptionHeightLandscape else maxCaptionHeightPortrait), ) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .verticalScroll(scrollState) - .navigationBarsPadding(), - text = caption, - style = ElementTheme.typography.fontBodyLgRegular, - ) + val textToRender = when { + formattedCaption != null -> formattedCaption + caption != null -> caption.safeLinkify().toSpannable() + else -> null + } + if (textToRender != null) { + CompositionLocalProvider( + LocalContentColor provides ElementTheme.colors.textPrimary, + LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular + ) { + EditorStyledText( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .verticalScroll(scrollState) + .navigationBarsPadding(), + text = textToRender, + style = ElementRichTextEditorStyle.textStyle(), + releaseOnDetach = false, + ) + } + } if (showBottomShadow) { Box( modifier = Modifier @@ -603,7 +648,6 @@ private val maxCaptionHeightLandscape = 128.dp @Composable private fun ThumbnailView( thumbnailSource: MediaSource?, - isVisible: Boolean, mediaInfo: MediaInfo, modifier: Modifier = Modifier, ) { @@ -611,21 +655,19 @@ private fun ThumbnailView( modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - if (isVisible) { - val mediaRequestData = MediaRequestData( - source = thumbnailSource, - kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType) - ) - val alpha = if (LocalInspectionMode.current) 0.1f else 1f - AsyncImage( - modifier = Modifier - .fillMaxSize() - .alpha(alpha), - model = mediaRequestData, - contentScale = ContentScale.Fit, - contentDescription = null, - ) - } + val mediaRequestData = MediaRequestData( + source = thumbnailSource, + kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType) + ) + val alpha = if (LocalInspectionMode.current) 0.1f else 1f + AsyncImage( + modifier = Modifier + .fillMaxSize() + .alpha(alpha), + model = mediaRequestData, + contentScale = ContentScale.Fit, + contentDescription = null, + ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt index f243ac4fd7..146ace8620 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -20,15 +20,18 @@ import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryData import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf class SingleMediaGalleryDataSource( private val data: GroupedMediaItems, ) : MediaGalleryDataSource { - override fun start() = Unit + override fun start(coroutineScope: CoroutineScope) = Unit override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) override fun getLastData(): AsyncData = AsyncData.Success(data) + override val isReady: Boolean = true + override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit override suspend fun deleteItem(eventId: EventId) = Unit diff --git a/libraries/mediaviewer/impl/src/main/res/values-be/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..9dc6e13043 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,4 @@ + + + "Інфармацыя пра файл" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-ca/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..44bf749c0f --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,21 @@ + + + "L\'arxiu s\'eliminarà de la sala i el membres no en tindran accés." + "Eliminar el document?" + "Comprova la connexió a internet i torna-ho a provar." + "Els documents, àudios i missatges de veu que s\'hagin penjat a la sala es mostraran aquí." + "Encara no s\'han pujat documents" + "Carregant documents…" + "Carregant multimèdia…" + "Documents" + "Multimèdia" + "Les imatges i vídeos penjats a la sala es mostraran aquí." + "Encara no s\'ha pujat multimèdia" + "Multimèdia i documents" + "Format del document" + "Nom del document" + "No hi ha més fitxers a mostrar" + "No hi ha més multimèdia a mostrar" + "Pujat per" + "Pujat el" + diff --git a/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml index bcb7b64237..654aa869a6 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml @@ -16,6 +16,7 @@ "Název souboru" "Žádné další soubory k zobrazení" "Žádná další média k zobrazení" + "Informace o souboru" "Nahrál(a)" "Nahráno" diff --git a/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml index 5d88458495..6108eab22d 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-da/translations.xml @@ -16,6 +16,7 @@ "Filnavn" "Ikke flere filer at vise" "Ikke flere medier at vise" + "Filoplysninger" "Uploadet af" "Uploadet på" diff --git a/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml index 63329a797d..f982b32433 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml @@ -16,6 +16,7 @@ "Failinimi" "Pole enam kuvatavaid faile" "Pole enam kuvatavat meediat" + "Faili teave" "Üleslaadija" "Üleslaaditud" diff --git a/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml index b3728cb9b7..05154b293c 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml @@ -16,6 +16,7 @@ "Tiedostonimi" "Ei enää näytettäviä tiedostoja" "Ei enää näytettävää mediaa" + "Tiedoston tiedot" "Lähettäjä" "Lähetetty" diff --git a/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml index b6334095ab..77372c1374 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml @@ -16,6 +16,7 @@ "Nom du fichier" "Il n’y a plus de fichiers à montrer" "Il n’y a plus de médias à montrer" + "Informations sur le fichier" "Envoyé par" "Envoyé le" diff --git a/libraries/mediaviewer/impl/src/main/res/values-hr/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-hr/translations.xml index 7637fbf350..2a417cc070 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-hr/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-hr/translations.xml @@ -16,6 +16,7 @@ "Naziv datoteke" "Nema više datoteka za prikaz" "Nema više medijskih sadržaja za prikaz" + "Informacije o datoteci" "Prenio/la" "Preneseno na" diff --git a/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml index 14cc11fb96..4f4d244d44 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml @@ -16,6 +16,7 @@ "Fájlnév" "Nincs több megjeleníthető fájl" "Nincs több megjeleníthető média" + "Fájlinformáció" "Feltöltötte:" "Feltöltve:" diff --git a/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml index 72701194fc..913329122e 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml @@ -16,6 +16,7 @@ "Nome del file" "Nessun altro file da mostrare" "Non ci sono più contenuti multimediali da mostrare" + "Informazioni sul file" "Caricato da" "Caricato il" diff --git a/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml index cb4da33df8..9e136b0884 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ja/translations.xml @@ -16,6 +16,7 @@ "ファイル名" "これ以上ファイルはありません" "これ以上メディアはありません" + "ファイル情報" "アップロード元" "アップロード先" diff --git a/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml index 3362171d27..0a14586e6c 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ko/translations.xml @@ -16,6 +16,7 @@ "파일 명" "더 이상 표시할 파일이 없습니다" "더 이상 보여줄 미디어가 없습니다" + "파일 정보" "에 의해 업로드됨" "에 업로드됨" diff --git a/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml index c398123873..a1f7a3bdf9 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-pl/translations.xml @@ -16,6 +16,7 @@ "Nazwa pliku" "Brak plików do pokazania" "Brak mediów do pokazania" + "Informacje pliku" "Przesłane przez" "Przesłane w dniu" diff --git a/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml index bfd1d49c2b..aa713361d0 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ro/translations.xml @@ -16,6 +16,7 @@ "Nume fișier" "Nu mai există fișiere de afișat" "Nu mai există conținut media de afișat" + "Informații despre fișier" "Încărcat de" "Încărcat la" diff --git a/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml index cdbe07f2e1..74b910faaa 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml @@ -16,6 +16,7 @@ "Имя файла" "Больше нет файлов для отображения" "Больше нет медиа для отображения" + "Информация о файле" "Загружено" "Загружено" diff --git a/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml index 45e1e1703b..ddc316fb4b 100644 --- a/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml +++ b/libraries/mediaviewer/impl/src/main/res/values-uk/translations.xml @@ -16,6 +16,7 @@ "Назва файлу" "Більше немає файлів для показу" "Більше немає медіа для показу" + "Інформація про файл" "Вивантажено користувачем" "Вивантажено" diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt index b848ea2a7b..e7d45f433a 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt @@ -33,16 +33,21 @@ import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.Rule import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class DefaultMediaViewerEntryPointTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun `test node builder`() = runTest { + Dispatchers.setMain(testCoroutineDispatchers().main) val entryPoint = DefaultMediaViewerEntryPoint() val mockMediaUri: Uri = mockk("localMediaUri") val localMediaFactory = FakeLocalMediaFactory(mockMediaUri) @@ -89,6 +94,7 @@ class DefaultMediaViewerEntryPointTest { @Test fun `test node builder avatar`() = runTest { + Dispatchers.setMain(testCoroutineDispatchers().main) val entryPoint = DefaultMediaViewerEntryPoint() val mockMediaUri: Uri = mockk("localMediaUri") val localMediaFactory = FakeLocalMediaFactory(mockMediaUri) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt index 6602f475eb..ab3ffa98c7 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/DefaultEventItemFactoryTest.kt @@ -62,7 +62,7 @@ class DefaultEventItemFactoryTest { fun `create check all null cases`() { val factory = createEventItemFactory() val contents = listOf( - CallNotifyContent(callIntent = CallIntent.VIDEO), + CallNotifyContent(callIntent = CallIntent.VIDEO, emptyList()), FailedToParseMessageLikeContent("", ""), FailedToParseStateContent("", "", ""), LegacyCallInviteContent, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt index c612bba1bc..5839f33ce6 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt @@ -13,19 +13,22 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.tests.testutils.lambda.lambdaError +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow class FakeMediaGalleryDataSource( + initialData: AsyncData = AsyncData.Uninitialized, + private val isReadyResult: () -> Boolean = { true }, private val startLambda: () -> Unit = { lambdaError() }, private val loadMoreLambda: (Timeline.PaginationDirection) -> Unit = { lambdaError() }, private val deleteItemLambda: (EventId) -> Unit = { lambdaError() }, - ) : MediaGalleryDataSource { - override fun start() = startLambda() +) : MediaGalleryDataSource { + override fun start(coroutineScope: CoroutineScope) = startLambda() - private val groupedMediaItemsFlow = MutableSharedFlow>( - replay = 1 - ) + private val groupedMediaItemsFlow = MutableStateFlow(initialData) + + override val isReady: Boolean get() = isReadyResult() override fun groupedMediaItemsFlow(): Flow> { return groupedMediaItemsFlow diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt index bb8419dde5..88aefb842c 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt @@ -80,7 +80,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) assertThat(sut.getLastData()).isEqualTo(AsyncData.Uninitialized) sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() @@ -95,7 +95,7 @@ class TimelineMediaGalleryDataSourceTest { ) assertThat(sut.getLastData().isSuccess()).isTrue() // Also test that starting again should have no effect - sut.start() + sut.start(backgroundScope) } } // Ensure that the timeline has been closed on flow completion @@ -117,7 +117,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { skipItems(2) sut.loadMore(Timeline.PaginationDirection.BACKWARDS) @@ -140,7 +140,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { skipItems(2) sut.deleteItem(AN_EVENT_ID) @@ -159,7 +159,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() assertThat(sut.getLastData().isLoading()).isTrue() @@ -181,7 +181,7 @@ class TimelineMediaGalleryDataSourceTest { roomCoroutineScope = backgroundScope, ) ) - sut.start() + sut.start(backgroundScope) sut.groupedMediaItemsFlow().test { assertThat(awaitItem().isLoading()).isTrue() assertThat(sut.getLastData().isLoading()).isTrue() @@ -235,6 +235,7 @@ class TimelineMediaGalleryDataSourceTest { filename = "body.jpg", fileSize = 888L, caption = "body.jpg caption", + formattedCaption = "formatted", mimeType = MimeTypes.Jpeg, formattedFileSize = "888 Bytes", fileExtension = "jpg", diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt index 4cbb35a85e..a9fbae9eec 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt @@ -6,13 +6,15 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -20,44 +22,38 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam 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.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -import org.junit.Rule import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class MediaDeleteConfirmationBottomSheetTest { - @get:Rule - val rule = createAndroidComposeRule() +class MediaDeleteConfirmationBottomSheetTest : RobolectricTest() { @Test - fun `clicking on Cancel invokes expected callback`() { + fun `clicking on Cancel invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDeleteConfirmation() ensureCalledOnce { callback -> - rule.setMediaDeleteConfirmationBottomSheet( + setMediaDeleteConfirmationBottomSheet( state = state, onDismiss = callback, ) - rule.clickOn(CommonStrings.action_cancel) + clickOn(CommonStrings.action_cancel) } } @Test - fun `clicking on Remove invokes expected callback`() { + fun `clicking on Remove invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDeleteConfirmation() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDeleteConfirmationBottomSheet( + setMediaDeleteConfirmationBottomSheet( state = state, onDelete = callback, ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists() - rule.clickOn(CommonStrings.action_remove) + onNodeWithText(activity!!.getString(CommonStrings.action_remove)).assertExists() + clickOn(CommonStrings.action_remove) } } } -private fun AndroidComposeTestRule.setMediaDeleteConfirmationBottomSheet( +private fun AndroidComposeUiTest.setMediaDeleteConfirmationBottomSheet( state: MediaBottomSheetState.DeleteConfirmation, onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index 21a06f9568..4ad348a847 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -6,111 +6,129 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.mediaviewer.impl.details import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent -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 MediaDetailsBottomSheetTest { - @get:Rule - val rule = createAndroidComposeRule() - +class MediaDetailsBottomSheetTest : RobolectricTest() { @Test @Config(qualifiers = "h1024dp") - fun `clicking on View in timeline invokes expected callback`() { + fun `clicking on View in timeline invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onViewInTimeline = callback, ) - rule.clickOn(CommonStrings.action_view_in_timeline) + clickOn(CommonStrings.action_view_in_timeline) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Share invokes expected callback`() { - val state = aMediaBottomSheetStateDetails() + fun `clicking on Share invokes expected callback`() = runAndroidComposeUiTest { + val state = aMediaBottomSheetStateDetails( + fromGallery = true, + ) ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onShare = callback, ) - rule.clickOn(CommonStrings.action_share) + clickOn(CommonStrings.action_share) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Forward invokes expected callback`() { + fun `item Share is not displayed when opened from the media viewer`() = runAndroidComposeUiTest { + setMediaDetailsBottomSheet( + state = aMediaBottomSheetStateDetails(), + ) + onNodeWithText(activity!!.getString(CommonStrings.action_share)).assertDoesNotExist() + } + + @Test + @Config(qualifiers = "h1024dp") + fun `clicking on Forward invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onForward = callback, ) - rule.clickOn(CommonStrings.action_forward) + clickOn(CommonStrings.action_forward) } } @Test @Config(qualifiers = "h1024dp") - fun `clicking on Download invokes expected callback`() { - val state = aMediaBottomSheetStateDetails() + fun `clicking on Download invokes expected callback`() = runAndroidComposeUiTest { + val state = aMediaBottomSheetStateDetails( + fromGallery = true, + ) ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onDownload = callback, ) - rule.clickOn(CommonStrings.action_download) + clickOn(CommonStrings.action_download) } } + @Test + @Config(qualifiers = "h1024dp") + fun `item Download is not displayed when opened from the media viewer`() = runAndroidComposeUiTest { + setMediaDetailsBottomSheet( + state = aMediaBottomSheetStateDetails(), + ) + onNodeWithText(activity!!.getString(CommonStrings.action_download)).assertDoesNotExist() + } + @Config(qualifiers = "h1024dp") @Test - fun `clicking on Delete invokes expected callback`() { + fun `clicking on Delete invokes expected callback`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails() ensureCalledOnceWithParam(state.eventId) { callback -> - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, onDelete = callback, ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_delete)).assertExists() - rule.clickOn(CommonStrings.action_delete) + onNodeWithText(activity!!.getString(CommonStrings.action_delete_file)).assertExists() + clickOn(CommonStrings.action_delete_file) } } @Config(qualifiers = "h1024dp") @Test - fun `Remove is not present if canDelete is false`() { + fun `Remove is not present if canDelete is false`() = runAndroidComposeUiTest { val state = aMediaBottomSheetStateDetails( canDelete = false, ) - rule.setMediaDetailsBottomSheet( + setMediaDetailsBottomSheet( state = state, ) - rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertDoesNotExist() + onNodeWithText(activity!!.getString(CommonStrings.action_remove)).assertDoesNotExist() } } -private fun AndroidComposeTestRule.setMediaDetailsBottomSheet( +private fun AndroidComposeUiTest.setMediaDetailsBottomSheet( state: MediaBottomSheetState.Details, onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(), onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(), diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index 929f2a970e..3ab95f2703 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -137,6 +137,7 @@ class MediaGalleryPresenterTest { val state = awaitItem() assertThat(state.mediaBottomSheetState).isEqualTo( MediaBottomSheetState.Details( + fromGallery = true, eventId = AN_EVENT_ID, canDelete = canDeleteOwn, mediaInfo = item.mediaInfo, @@ -184,6 +185,7 @@ class MediaGalleryPresenterTest { val state = awaitItem() assertThat(state.mediaBottomSheetState).isEqualTo( MediaBottomSheetState.Details( + fromGallery = true, eventId = AN_EVENT_ID, canDelete = canDeleteOther, mediaInfo = item.mediaInfo, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt index cd7b7003af..cdc5f7220d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt @@ -18,16 +18,14 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -@RunWith(RobolectricTestRunner::class) -class AndroidLocalMediaActionsTest { +class AndroidLocalMediaActionsTest : RobolectricTest() { @Test fun `present - AndroidLocalMediaAction configure`() = runTest { val sut = createAndroidLocalMediaActions() diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt index f01ac1d749..246ae50b4d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt @@ -8,7 +8,9 @@ package io.element.android.libraries.mediaviewer.impl.local +import android.graphics.Bitmap import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.file.getMimeType import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaFile @@ -18,13 +20,15 @@ import io.element.android.libraries.matrix.test.media.FakeMediaFile import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment +import java.io.File +import java.io.FileOutputStream + +class AndroidLocalMediaFactoryTest : RobolectricTest() { + private val context = RuntimeEnvironment.getApplication() -@RunWith(RobolectricTestRunner::class) -class AndroidLocalMediaFactoryTest { @Test fun `test AndroidLocalMediaFactory`() { val sut = createAndroidLocalMediaFactory() @@ -58,13 +62,34 @@ class AndroidLocalMediaFactoryTest { ) } + @Test + fun `createFromUri detects image mime type from content when picker mime type is generic`() { + val imageFile = File(context.cacheDir, "picked-media").apply { + FileOutputStream(this).use { outputStream -> + Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + .compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } + } + + val result = createAndroidLocalMediaFactory().createFromUri( + uri = imageFile.toURI().toString().let(android.net.Uri::parse), + mimeType = MimeTypes.OctetStream, + name = imageFile.name, + formattedFileSize = null, + ) + + assertThat(context.getMimeType(result.uri)).isNull() + assertThat(result.info.mimeType).isEqualTo(MimeTypes.Png) + assertThat(result.info.fileExtension).isEmpty() + } + private fun aMediaFile(): MediaFile { return FakeMediaFile("aPath") } private fun createAndroidLocalMediaFactory(): AndroidLocalMediaFactory { return AndroidLocalMediaFactory( - RuntimeEnvironment.getApplication(), + context, FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation() ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt index 3c5c91589c..6f0d85894a 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/util/FileExtensionExtractorWithValidationTest.kt @@ -9,12 +9,10 @@ package io.element.android.libraries.mediaviewer.impl.util import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class FileExtensionExtractorWithValidationTest { +class FileExtensionExtractorWithValidationTest : RobolectricTest() { @Test fun `test FileExtensionExtractor with validation OK`() { val sut = FileExtensionExtractorWithValidation() diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt index 44eed2733f..d7ba547ee4 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt @@ -50,7 +50,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.setup() + sut.setup(backgroundScope) startLambda.assertions().isCalledOnce() } @@ -62,15 +62,20 @@ class MediaViewerDataSourceTest { @Test fun `test dataFlow uninitialized, loading and error`() = runTest { - val galleryDataSource = FakeMediaGalleryDataSource() + val galleryDataSource = FakeMediaGalleryDataSource( + initialData = AsyncData.Uninitialized, + ) val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { + // The flow starts with an empty result + assertThat(awaitItem()).isEmpty() galleryDataSource.emitGroupedMediaItems(AsyncData.Uninitialized) assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) galleryDataSource.emitGroupedMediaItems(AsyncData.Loading()) - assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) + // No items emitted, we were already loading data + ensureAllEventsConsumed() galleryDataSource.emitGroupedMediaItems(AsyncData.Failure(AN_EXCEPTION)) assertThat(awaitItem().first()).isEqualTo(MediaViewerPageData.Failure(AN_EXCEPTION)) } @@ -82,7 +87,7 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -102,7 +107,8 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { + skipItems(1) galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -141,7 +147,8 @@ class MediaViewerDataSourceTest { mode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.Media), galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { + skipItems(1) galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -163,7 +170,8 @@ class MediaViewerDataSourceTest { mode = MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.Media), galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { + skipItems(1) galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -184,7 +192,8 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { + skipItems(1) galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -217,7 +226,8 @@ class MediaViewerDataSourceTest { val sut = createMediaViewerDataSource( galleryDataSource = galleryDataSource, ) - sut.dataFlow().test { + sut.dataFlow.test { + skipItems(1) galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -241,7 +251,8 @@ class MediaViewerDataSourceTest { galleryDataSource = galleryDataSource, mediaLoader = mediaLoader, ) - sut.dataFlow().test { + sut.dataFlow.test { + skipItems(1) galleryDataSource.emitGroupedMediaItems( AsyncData.Success( aGroupedMediaItems( @@ -271,6 +282,7 @@ class MediaViewerDataSourceTest { mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(), localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl), ) = MediaViewerDataSource( + coroutineScope = backgroundScope, mode = mode, dispatcher = testCoroutineDispatchers().computation, galleryDataSource = galleryDataSource, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index caacb03804..459d30b8d5 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader @@ -51,6 +52,8 @@ import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -97,6 +100,8 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + + cancelAndIgnoreRemainingEvents() } } @@ -120,6 +125,8 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isFalse() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + + cancelAndIgnoreRemainingEvents() } } @@ -143,6 +150,8 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + + cancelAndIgnoreRemainingEvents() } } @@ -167,6 +176,8 @@ class MediaViewerPresenterTest { assertThat(initialState.snackbarMessage).isNull() assertThat(initialState.canShowInfo).isTrue() assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + + cancelAndIgnoreRemainingEvents() } } @@ -555,6 +566,8 @@ class MediaViewerPresenterTest { ) val finalState = awaitItem() assertThat(finalState.currentIndex).isEqualTo(1) + + cancelAndIgnoreRemainingEvents() } } @@ -578,35 +591,37 @@ class MediaViewerPresenterTest { mode: MediaViewerEntryPoint.MediaViewerMode, expectedSnackbarResId: Int, ) = runTest { + val image = anImage.copy(eventId = AN_EVENT_ID) val mediaGalleryDataSource = FakeMediaGalleryDataSource( + initialData = AsyncData.Success( + if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { + GroupedMediaItems( + imageAndVideoItems = persistentListOf(), + fileItems = persistentListOf(aForwardLoadingIndicator, image, aBackwardLoadingIndicator), + ) + } else { + GroupedMediaItems( + imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, image, aBackwardLoadingIndicator), + fileItems = persistentListOf(), + ) + } + ), startLambda = { }, ) val presenter = createMediaViewerPresenter( + eventId = AN_EVENT_ID, localMediaFactory = localMediaFactory, mode = mode, mediaGalleryDataSource = mediaGalleryDataSource, ) presenter.test { - awaitFirstItem() - mediaGalleryDataSource.emitGroupedMediaItems( - AsyncData.Success( - if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { - GroupedMediaItems( - imageAndVideoItems = persistentListOf(), - fileItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator), - ) - } else { - GroupedMediaItems( - imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator), - fileItems = persistentListOf(), - ) - } - ) - ) - val updatedState = awaitItem() - // User navigate to the last item (forward loading indicator) + val updatedState = awaitFirstItem() + + advanceUntilIdle() + runCurrent() + // User navigate to the first item (forward loading indicator) updatedState.eventSink( - MediaViewerEvent.OnNavigateTo(2) + MediaViewerEvent.OnNavigateTo(0) ) // data source claims that there is no more items to load forward mediaGalleryDataSource.emitGroupedMediaItems( @@ -614,21 +629,19 @@ class MediaViewerPresenterTest { if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), - fileItems = persistentListOf(aBackwardLoadingIndicator, anImage), + fileItems = persistentListOf(image, aBackwardLoadingIndicator), ) } else { GroupedMediaItems( - imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage), + imageAndVideoItems = persistentListOf(image, aBackwardLoadingIndicator), fileItems = persistentListOf(), ) } ) ) - var stateWithSnackbar = awaitItem() - while (stateWithSnackbar.snackbarMessage == null) { - stateWithSnackbar = awaitItem() - } - assertThat(stateWithSnackbar.snackbarMessage.messageResId).isEqualTo(expectedSnackbarResId) + skipItems(2) + val stateWithSnackbar = awaitItem() + assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId) } } @@ -667,42 +680,41 @@ class MediaViewerPresenterTest { if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), - fileItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator), + fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), ) } else { GroupedMediaItems( - imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator), + imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), fileItems = persistentListOf(), ) } ) ) val updatedState = awaitItem() - // User navigate to the first item (backward loading indicator) + // User navigate to the last item (backward loading indicator) updatedState.eventSink( - MediaViewerEvent.OnNavigateTo(0) + MediaViewerEvent.OnNavigateTo(2) ) + skipItems(1) // data source claims that there is no more items to load backward mediaGalleryDataSource.emitGroupedMediaItems( AsyncData.Success( if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) { GroupedMediaItems( imageAndVideoItems = persistentListOf(), - fileItems = persistentListOf(anImage, aForwardLoadingIndicator), + fileItems = persistentListOf(aForwardLoadingIndicator, anImage), ) } else { GroupedMediaItems( - imageAndVideoItems = persistentListOf(anImage, aForwardLoadingIndicator), + imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage), fileItems = persistentListOf(), ) } ) ) - var stateWithSnackbar = awaitItem() - while (stateWithSnackbar.snackbarMessage == null) { - stateWithSnackbar = awaitItem() - } - assertThat(stateWithSnackbar.snackbarMessage.messageResId).isEqualTo(expectedSnackbarResId) + skipItems(1) + val stateWithSnackbar = awaitItem() + assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId) } } @@ -710,21 +722,19 @@ class MediaViewerPresenterTest { fun `present - no snackbar displayed when there is no more items but not displaying a loading item`() = runTest { val mediaGalleryDataSource = FakeMediaGalleryDataSource( startLambda = { }, + initialData = AsyncData.Success( + GroupedMediaItems( + imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, anImage.copy(eventId = AN_EVENT_ID_2), aBackwardLoadingIndicator), + fileItems = persistentListOf(), + ) + ) ) val presenter = createMediaViewerPresenter( + eventId = AN_EVENT_ID, localMediaFactory = localMediaFactory, mediaGalleryDataSource = mediaGalleryDataSource, ) presenter.test { - awaitFirstItem() - mediaGalleryDataSource.emitGroupedMediaItems( - AsyncData.Success( - GroupedMediaItems( - imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator), - fileItems = persistentListOf(), - ) - ) - ) val updatedState = awaitItem() // User navigate to the media updatedState.eventSink( @@ -777,6 +787,51 @@ class MediaViewerPresenterTest { } } + @Test + fun `present - receiving loading items with different timestamps emits different items too`() = runTest { + val loadMoreLambda = lambdaRecorder { } + val mediaGalleryDataSource = FakeMediaGalleryDataSource( + startLambda = { }, + loadMoreLambda = loadMoreLambda, + ) + val presenter = createMediaViewerPresenter( + localMediaFactory = localMediaFactory, + mediaGalleryDataSource = mediaGalleryDataSource, + ) + val anImage = aMediaItemImage( + mediaSourceUrl = aUrl, + ) + presenter.test { + awaitFirstItem() + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + GroupedMediaItems( + imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator), + fileItems = persistentListOf(), + ) + ) + ) + val updatedState = awaitItem() + + // Get the exact same items, but with new timestamps for the loading indicators + mediaGalleryDataSource.emitGroupedMediaItems( + AsyncData.Success( + GroupedMediaItems( + imageAndVideoItems = persistentListOf( + aForwardLoadingIndicator.copy(timestamp = 1234L), + anImage, + aBackwardLoadingIndicator.copy(timestamp = 1234L), + ), + fileItems = persistentListOf(), + ) + ) + ) + + // We should get a new list of items, which should not be equal to the previous one + assertThat(updatedState.listData).isNotEqualTo(awaitItem().listData) + } + } + @Test fun `present - view in timeline hides the bottom sheet and invokes the navigator`() = runTest { val onViewInTimelineClickLambda = lambdaRecorder { } @@ -797,6 +852,7 @@ class MediaViewerPresenterTest { presenter.test { val initialState = awaitItem() initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData())) + skipItems(1) val withBottomSheetState = awaitItem() assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) initialState.eventSink(MediaViewerEvent.ViewInTimeline(AN_EVENT_ID)) @@ -826,6 +882,7 @@ class MediaViewerPresenterTest { presenter.test { val initialState = awaitItem() initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData())) + skipItems(1) val withBottomSheetState = awaitItem() assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) initialState.eventSink(MediaViewerEvent.Forward(AN_EVENT_ID)) @@ -857,6 +914,7 @@ class MediaViewerPresenterTest { presenter.test { val initialState = awaitItem() initialState.eventSink(MediaViewerEvent.OpenInfo(aMediaViewerPageData())) + skipItems(1) val withBottomSheetState = awaitItem() assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.Details::class.java) initialState.eventSink(MediaViewerEvent.Forward(AN_EVENT_ID)) @@ -895,6 +953,7 @@ internal fun TestScope.createMediaViewerPresenter( ), navigator = mediaViewerNavigator, dataSource = MediaViewerDataSource( + coroutineScope = backgroundScope, mode = mode, dispatcher = testCoroutineDispatchers().computation, galleryDataSource = mediaGalleryDataSource, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index 9eded788aa..d2780bb408 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -6,20 +6,28 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri import androidx.activity.ComponentActivity import androidx.annotation.StringRes +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertHasClickAction -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.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.libraries.ui.strings.CommonStrings @@ -28,36 +36,36 @@ 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 io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.setSafeContent import io.mockk.mockk -import org.junit.Rule +import kotlinx.coroutines.delay import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config +import kotlin.time.Duration.Companion.milliseconds -@RunWith(AndroidJUnit4::class) -class MediaViewerViewTest { - @get:Rule val rule = createAndroidComposeRule() - +class MediaViewerViewTest : RobolectricTest() { private val mockMediaUrl: Uri = mockk("localMediaUri") @Test - fun `clicking on back invokes expected callback`() { + fun `clicking on back invokes expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMediaViewerView( + setMediaViewerView( state = state, onBackClick = callback, ) - rule.pressBack() + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + + pressBack() } eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), ) ) @@ -103,47 +111,28 @@ class MediaViewerViewTest { data: MediaViewerPageData.MediaViewerData, @StringRes contentDescriptionRes: Int, expectedEvent: MediaViewerEvent, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - val contentDescription = rule.activity.getString(contentDescriptionRes) - rule.onNodeWithContentDescription(contentDescription).performClick() + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + + val contentDescription = activity!!.getString(contentDescriptionRes) + onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(data), expectedEvent, ) ) } - @Test - @Config(qualifiers = "h1024dp") - fun `clicking on download emits expected Event`() { - val data = aMediaViewerPageData() - testBottomSheetAction( - data, - CommonStrings.action_download, - MediaViewerEvent.SaveOnDisk(data), - ) - } - - @Test - @Config(qualifiers = "h1024dp") - fun `clicking on share emits expected Event`() { - val data = aMediaViewerPageData() - testBottomSheetAction( - data, - CommonStrings.action_share, - MediaViewerEvent.Share(data), - ) - } - @Config(qualifiers = "h1024dp") @Test fun `clicking on open in emits expected Event`() { @@ -159,19 +148,18 @@ class MediaViewerViewTest { data: MediaViewerPageData.MediaViewerData, @StringRes textRes: Int, expectedEvent: MediaViewerEvent, - ) { + ) = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), mediaBottomSheetState = aMediaBottomSheetStateDetails(), eventSink = eventsRecorder ), ) - rule.clickOn(textRes) + clickOn(textRes) eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(data), expectedEvent, ) @@ -179,72 +167,74 @@ class MediaViewerViewTest { } @Test - fun `clicking on image hides the overlay`() { + fun `clicking on image hides the overlay`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) - rule.setMediaViewerView( + setMediaViewerView( state = state, ) // Ensure that the action are visible - val contentDescription = rule.activity.getString(CommonStrings.action_share) - rule.onNodeWithContentDescription(contentDescription) + val resources = activity!!.resources + val contentDescription = resources.getString(CommonStrings.action_share) + onNodeWithContentDescription(contentDescription) .assertExists() .assertHasClickAction() - val imageContentDescription = rule.activity.getString(CommonStrings.common_image) - rule.onNodeWithContentDescription(imageContentDescription).performClick() + val imageContentDescription = resources.getString(CommonStrings.common_image) + onNodeWithContentDescription(imageContentDescription).performClick() // Give time for the animation (? since even by removing AnimatedVisibility it still fails) - rule.mainClock.advanceTimeBy(1_000) - rule.onNodeWithContentDescription(contentDescription) + mainClock.advanceTimeBy(1_000) + onNodeWithContentDescription(contentDescription) .assertDoesNotExist() eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), ) ) } @Test - fun `clicking swipe on the image invokes the expected callback`() { + fun `clicking swipe on the image invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val state = aMediaViewerState( eventSink = eventsRecorder ) ensureCalledOnce { callback -> - rule.setMediaViewerView( + setMediaViewerView( state = state, onBackClick = callback, ) - val imageContentDescription = rule.activity.getString(CommonStrings.common_image) - rule.onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } - rule.mainClock.advanceTimeBy(1_000) + val imageContentDescription = activity!!.getString(CommonStrings.common_image) + onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } + mainClock.advanceTimeBy(1_000) } eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(state.listData.first() as MediaViewerPageData.MediaViewerData), ) ) } @Test - fun `error case, click on retry emits the expected Event`() { + fun `error case, click on retry emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val data = aMediaViewerPageData( downloadedMedia = AsyncData.Failure(IllegalStateException("error")), ) - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_retry) + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + + clickOn(CommonStrings.action_retry) eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(data), MediaViewerEvent.LoadMedia(data), ) @@ -252,29 +242,86 @@ class MediaViewerViewTest { } @Test - fun `error case, click on cancel emits the expected Event`() { + fun `error case, click on cancel emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() val data = aMediaViewerPageData( downloadedMedia = AsyncData.Failure(IllegalStateException("error")), ) - rule.setMediaViewerView( + setMediaViewerView( aMediaViewerState( listData = listOf(data), eventSink = eventsRecorder ), ) - rule.clickOn(CommonStrings.action_cancel) + + // Wait for enough time for the onVisibilityChanged modifier to trigger + mainClock.advanceTimeBy(200) + + clickOn(CommonStrings.action_cancel) eventsRecorder.assertList( listOf( - MediaViewerEvent.OnNavigateTo(0), MediaViewerEvent.LoadMedia(data), MediaViewerEvent.ClearLoadingError(data) ) ) } + + @Test + fun `loading event after an error triggers load more Event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val states = listOf( + aMediaViewerState( + listData = listOf(aMediaViewerPageDataLoading(timestamp = 0L)), + eventSink = eventsRecorder, + ), + aMediaViewerState( + listData = listOf(MediaViewerPageData.Failure(IllegalStateException("error"))), + eventSink = eventsRecorder, + ), + aMediaViewerState( + listData = listOf(aMediaViewerPageDataLoading(timestamp = 0L)), + eventSink = eventsRecorder, + ), + // This one should be ignored since it has the same timestamp as the last one, it should not trigger a recomposition + aMediaViewerState( + listData = listOf(aMediaViewerPageDataLoading(timestamp = 0L)), + eventSink = eventsRecorder, + ), + ) + setSafeContent { + // Iterate over the states with a delay to give the view some time to trigger the `LoadMore` Event + var state by remember { mutableStateOf(states.first()) } + LaunchedEffect(Unit) { + val iterator = states.iterator() + while (iterator.hasNext()) { + delay(200.milliseconds) + state = iterator.next() + } + } + MediaViewerView( + state = state, + textFileViewer = { _, _ -> }, + onBackClick = EnsureNeverCalled(), + audioFocus = null, + ) + } + + // Advance time to let the states update + mainClock.advanceTimeBy(3_000) + + // `LoadMore` should be called twice, once for the first loading state, and once for the second one even though they have the same timestamp because + // of the intermediate error state. + // The third one will be ignored since it has the same timestamp as the second one and it'll be discarded by the Compose's equality diffing. + eventsRecorder.assertList( + listOf( + MediaViewerEvent.LoadMore(direction = Timeline.PaginationDirection.BACKWARDS), + MediaViewerEvent.LoadMore(direction = Timeline.PaginationDirection.BACKWARDS), + ) + ) + } } -private fun AndroidComposeTestRule.setMediaViewerView( +private fun AndroidComposeUiTest.setMediaViewerView( state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt index c6460cb70a..8c0a7c05d0 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt @@ -37,9 +37,9 @@ class SingleMediaGalleryDataSourceTest { val warmUpRule = WarmUpRule() @Test - fun `function start is no op`() { + fun `function start is no op`() = runTest { val sut = SingleMediaGalleryDataSource(aGroupedMediaItems()) - sut.start() + sut.start(backgroundScope) } @Test diff --git a/libraries/mediaviewer/test/build.gradle.kts b/libraries/mediaviewer/test/build.gradle.kts index 1918714d7b..87665e6d69 100644 --- a/libraries/mediaviewer/test/build.gradle.kts +++ b/libraries/mediaviewer/test/build.gradle.kts @@ -18,6 +18,7 @@ android { dependencies { api(projects.libraries.mediaviewer.impl) + implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.tests.testutils) implementation(projects.libraries.matrix.api) diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt index faa27fd0e3..8dbee232df 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt @@ -41,6 +41,7 @@ class FakeLocalMediaFactory( filename = safeName, fileSize = null, caption = null, + formattedCaption = null, mimeType = mimeType ?: fallbackMimeType, formattedFileSize = formattedFileSize ?: fallbackFileSize, fileExtension = fileExtensionExtractor.extractFromName(safeName), diff --git a/libraries/network/build.gradle.kts b/libraries/network/build.gradle.kts index 72e7eb05f2..20f75091d9 100644 --- a/libraries/network/build.gradle.kts +++ b/libraries/network/build.gradle.kts @@ -24,6 +24,7 @@ android { setupDependencyInjection() dependencies { + implementation(libs.coroutines.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.di) diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt index 88240c3d65..39a54dca61 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.network import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.Provider import io.element.android.libraries.androidutils.json.JsonProvider import io.element.android.libraries.core.uri.ensureTrailingSlash import okhttp3.MediaType.Companion.toMediaType @@ -19,8 +18,8 @@ import retrofit2.converter.kotlinx.serialization.asConverterFactory @Inject class RetrofitFactory( - private val okHttpClient: Provider, - private val json: Provider, + private val okHttpClient: () -> OkHttpClient, + private val json: () -> JsonProvider, ) { fun create(baseUrl: String): Retrofit = Retrofit.Builder() .baseUrl(baseUrl.ensureTrailingSlash()) diff --git a/libraries/oidc/api/build.gradle.kts b/libraries/oauth/api/build.gradle.kts similarity index 87% rename from libraries/oidc/api/build.gradle.kts rename to libraries/oauth/api/build.gradle.kts index 8cc0125142..8c863db0bf 100644 --- a/libraries/oidc/api/build.gradle.kts +++ b/libraries/oauth/api/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } android { - namespace = "io.element.android.libraries.oidc.api" + namespace = "io.element.android.libraries.oauth.api" } dependencies { diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthAction.kt similarity index 54% rename from libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt rename to libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthAction.kt index d7c061ab25..033516b93d 100644 --- a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcAction.kt +++ b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthAction.kt @@ -6,9 +6,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.api +package io.element.android.libraries.oauth.api -sealed interface OidcAction { - data class GoBack(val toUnblock: Boolean = false) : OidcAction - data class Success(val url: String) : OidcAction +sealed interface OAuthAction { + data class GoBack(val toUnblock: Boolean = false) : OAuthAction + data class Success(val url: String) : OAuthAction } diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt similarity index 63% rename from libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt rename to libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt index 17340eb5ec..c6791e3197 100644 --- a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcActionFlow.kt +++ b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt @@ -6,12 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.api +package io.element.android.libraries.oauth.api import kotlinx.coroutines.flow.FlowCollector -interface OidcActionFlow { - fun post(oidcAction: OidcAction) - suspend fun collect(collector: FlowCollector) +interface OAuthActionFlow { + fun post(oAuthAction: OAuthAction) + suspend fun collect(collector: FlowCollector) fun reset() } diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt similarity index 68% rename from libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt rename to libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt index 97fa1baa27..2091a86db4 100644 --- a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcIntentResolver.kt +++ b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.api +package io.element.android.libraries.oauth.api import android.content.Intent -interface OidcIntentResolver { - fun resolve(intent: Intent): OidcAction? +interface OAuthIntentResolver { + fun resolve(intent: Intent): OAuthAction? } diff --git a/libraries/oidc/impl/build.gradle.kts b/libraries/oauth/impl/build.gradle.kts similarity index 93% rename from libraries/oidc/impl/build.gradle.kts rename to libraries/oauth/impl/build.gradle.kts index e11ce11c70..d051c06497 100644 --- a/libraries/oidc/impl/build.gradle.kts +++ b/libraries/oauth/impl/build.gradle.kts @@ -16,7 +16,7 @@ plugins { } android { - namespace = "io.element.android.libraries.oidc.impl" + namespace = "io.element.android.libraries.oauth.impl" testOptions { unitTests { @@ -39,7 +39,7 @@ dependencies { implementation(platform(libs.network.retrofit.bom)) implementation(libs.network.retrofit) implementation(libs.serialization.json) - api(projects.libraries.oidc.api) + api(projects.libraries.oauth.api) testCommonDependencies(libs) testImplementation(projects.libraries.matrix.test) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt b/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlow.kt similarity index 58% rename from libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt rename to libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlow.kt index 6096ef7eef..6b23676059 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt +++ b/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlow.kt @@ -6,26 +6,26 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.impl +package io.element.android.libraries.oauth.impl import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.MutableStateFlow @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultOidcActionFlow : OidcActionFlow { - private val mutableStateFlow = MutableStateFlow(null) +class DefaultOAuthActionFlow : OAuthActionFlow { + private val mutableStateFlow = MutableStateFlow(null) - override fun post(oidcAction: OidcAction) { - mutableStateFlow.value = oidcAction + override fun post(oAuthAction: OAuthAction) { + mutableStateFlow.value = oAuthAction } - override suspend fun collect(collector: FlowCollector) { + override suspend fun collect(collector: FlowCollector) { mutableStateFlow.collect(collector) } diff --git a/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolver.kt b/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolver.kt new file mode 100644 index 0000000000..c2a29e228c --- /dev/null +++ b/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolver.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2023-2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.oauth.impl + +import android.content.Intent +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthIntentResolver + +@ContributesBinding(AppScope::class) +class DefaultOAuthIntentResolver( + private val oAuthUrlParser: OAuthUrlParser, +) : OAuthIntentResolver { + override fun resolve(intent: Intent): OAuthAction? { + return oAuthUrlParser.parse(intent.dataString.orEmpty()) + } +} diff --git a/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/OAuthUrlParser.kt b/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/OAuthUrlParser.kt new file mode 100644 index 0000000000..7e2c4d03f5 --- /dev/null +++ b/libraries/oauth/impl/src/main/kotlin/io/element/android/libraries/oauth/impl/OAuthUrlParser.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2023-2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.oauth.impl + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.matrix.api.auth.OAuthRedirectUrlProvider +import io.element.android.libraries.oauth.api.OAuthAction +import timber.log.Timber + +fun interface OAuthUrlParser { + fun parse(url: String): OAuthAction? +} + +/** + * Simple parser for OAuth url interception. + * TODO Find documentation about the format. + */ +@ContributesBinding(AppScope::class) +class DefaultOAuthUrlParser( + private val oAuthRedirectUrlProvider: OAuthRedirectUrlProvider, +) : OAuthUrlParser { + /** + * Return a [OAuthAction], or null if the url is not an OAuth url. + * Note: + * When user press button "Cancel", we get the url: + * `io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO` + * On success, we get: + * `io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB` + */ + override fun parse(url: String): OAuthAction? { + if (url.startsWith(oAuthRedirectUrlProvider.provide()).not()) return null + if (url.contains("error=access_denied")) return OAuthAction.GoBack() + if (url.contains("code=")) return OAuthAction.Success(url) + + // Other cases are not supported, log an error and return null + Timber.w("Unsupported OAuth url") + return null + } +} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt b/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlowTest.kt similarity index 58% rename from libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt rename to libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlowTest.kt index 387b9aceb0..817487b3ff 100644 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt +++ b/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthActionFlowTest.kt @@ -1,34 +1,33 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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.oidc.impl +package io.element.android.libraries.oauth.impl import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.oidc.api.OidcAction +import io.element.android.libraries.oauth.api.OAuthAction import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Test -class DefaultOidcActionFlowTest { +class DefaultOAuthActionFlowTest { @Test fun `collect gets all the posted events`() = runTest { - val data = mutableListOf() - val sut = DefaultOidcActionFlow() + val data = mutableListOf() + val sut = DefaultOAuthActionFlow() backgroundScope.launch { sut.collect { action -> data.add(action) } } - sut.post(OidcAction.GoBack()) + sut.post(OAuthAction.GoBack()) delay(1) sut.reset() delay(1) - assertThat(data).containsExactly(OidcAction.GoBack(), null) + assertThat(data).containsExactly(OAuthAction.GoBack(), null) } } diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt b/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolverTest.kt similarity index 54% rename from libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt rename to libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolverTest.kt index 64068030d7..a93debd46a 100644 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolverTest.kt +++ b/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthIntentResolverTest.kt @@ -1,69 +1,64 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * 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.oidc.impl +package io.element.android.libraries.oauth.impl import android.app.Activity import android.content.Intent import androidx.core.net.toUri import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider -import io.element.android.libraries.oidc.api.OidcAction -import org.junit.Assert.assertThrows +import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -@RunWith(RobolectricTestRunner::class) -class DefaultOidcIntentResolverTest { +class DefaultOAuthIntentResolverTest : RobolectricTest() { @Test - fun `test resolve oidc go back`() { - val sut = createDefaultOidcIntentResolver() + fun `test resolve OAuth go back`() { + val sut = createDefaultOAuthIntentResolver() val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW data = "io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO".toUri() } val result = sut.resolve(intent) - assertThat(result).isEqualTo(OidcAction.GoBack()) + assertThat(result).isEqualTo(OAuthAction.GoBack()) } @Test - fun `test resolve oidc success`() { - val sut = createDefaultOidcIntentResolver() + fun `test resolve OAuth success`() { + val sut = createDefaultOAuthIntentResolver() val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW data = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB".toUri() } val result = sut.resolve(intent) assertThat(result).isEqualTo( - OidcAction.Success( + OAuthAction.Success( url = "io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" ) ) } @Test - fun `test resolve oidc invalid`() { - val sut = createDefaultOidcIntentResolver() + fun `test resolve OAuth invalid`() { + val sut = createDefaultOAuthIntentResolver() val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { action = Intent.ACTION_VIEW data = "io.element.android:/invalid".toUri() } - assertThrows(IllegalStateException::class.java) { - sut.resolve(intent) - } + val result = sut.resolve(intent) + assertThat(result).isNull() } - private fun createDefaultOidcIntentResolver(): DefaultOidcIntentResolver { - return DefaultOidcIntentResolver( - oidcUrlParser = DefaultOidcUrlParser( - oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), + private fun createDefaultOAuthIntentResolver(): DefaultOAuthIntentResolver { + return DefaultOAuthIntentResolver( + oAuthUrlParser = DefaultOAuthUrlParser( + oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), ), ) } diff --git a/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthUrlParserTest.kt b/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthUrlParserTest.kt new file mode 100644 index 0000000000..7dde7356d4 --- /dev/null +++ b/libraries/oauth/impl/src/test/kotlin/io/element/android/libraries/oauth/impl/DefaultOAuthUrlParserTest.kt @@ -0,0 +1,55 @@ +/* + * 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.oauth.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.auth.FAKE_REDIRECT_URL +import io.element.android.libraries.matrix.test.auth.FakeOAuthRedirectUrlProvider +import io.element.android.libraries.oauth.api.OAuthAction +import org.junit.Test + +class DefaultOAuthUrlParserTest { + @Test + fun `test empty url`() { + val sut = createDefaultOAuthUrlParser() + assertThat(sut.parse("")).isNull() + } + + @Test + fun `test regular url`() { + val sut = createDefaultOAuthUrlParser() + assertThat(sut.parse("https://matrix.org")).isNull() + } + + @Test + fun `test cancel url`() { + val sut = createDefaultOAuthUrlParser() + val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO" + assertThat(sut.parse(aCancelUrl)).isEqualTo(OAuthAction.GoBack()) + } + + @Test + fun `test success url`() { + val sut = createDefaultOAuthUrlParser() + val aSuccessUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + assertThat(sut.parse(aSuccessUrl)).isEqualTo(OAuthAction.Success(aSuccessUrl)) + } + + @Test + fun `test unknown url`() { + val sut = createDefaultOAuthUrlParser() + val anUnknownUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" + assertThat(sut.parse(anUnknownUrl)).isNull() + } + + private fun createDefaultOAuthUrlParser(): DefaultOAuthUrlParser { + return DefaultOAuthUrlParser( + oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(), + ) + } +} diff --git a/libraries/oidc/test/build.gradle.kts b/libraries/oauth/test/build.gradle.kts similarity index 80% rename from libraries/oidc/test/build.gradle.kts rename to libraries/oauth/test/build.gradle.kts index efe32d404a..6850653ddc 100644 --- a/libraries/oidc/test/build.gradle.kts +++ b/libraries/oauth/test/build.gradle.kts @@ -11,11 +11,11 @@ plugins { } android { - namespace = "io.element.android.libraries.oidc.test" + namespace = "io.element.android.libraries.oauth.test" } dependencies { implementation(libs.coroutines.core) - api(projects.libraries.oidc.api) + api(projects.libraries.oauth.api) implementation(projects.tests.testutils) } diff --git a/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt b/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthIntentResolver.kt similarity index 50% rename from libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt rename to libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthIntentResolver.kt index 45b400868b..893289023e 100644 --- a/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/FakeOidcIntentResolver.kt +++ b/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthIntentResolver.kt @@ -6,17 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.test +package io.element.android.libraries.oauth.test import android.content.Intent -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcIntentResolver +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthIntentResolver import io.element.android.tests.testutils.lambda.lambdaError -class FakeOidcIntentResolver( - private val resolveResult: (Intent) -> OidcAction? = { lambdaError() } -) : OidcIntentResolver { - override fun resolve(intent: Intent): OidcAction? { +class FakeOAuthIntentResolver( + private val resolveResult: (Intent) -> OAuthAction? = { lambdaError() } +) : OAuthIntentResolver { + override fun resolve(intent: Intent): OAuthAction? { return resolveResult(intent) } } diff --git a/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.kt b/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.kt new file mode 100644 index 0000000000..5a5ca4369e --- /dev/null +++ b/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.kt @@ -0,0 +1,33 @@ +/* + * 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.libraries.oauth.test.customtab + +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * This is actually a copy of DefaultOAuthActionFlow. + */ +class FakeOAuthActionFlow : OAuthActionFlow { + private val mutableStateFlow = MutableStateFlow(null) + + override fun post(oAuthAction: OAuthAction) { + mutableStateFlow.value = oAuthAction + } + + override suspend fun collect(collector: FlowCollector) { + mutableStateFlow.collect(collector) + } + + override fun reset() { + mutableStateFlow.value = null + } +} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt deleted file mode 100644 index 2a16030b3b..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcIntentResolver.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.oidc.impl - -import android.content.Intent -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcIntentResolver - -@ContributesBinding(AppScope::class) -class DefaultOidcIntentResolver( - private val oidcUrlParser: OidcUrlParser, -) : OidcIntentResolver { - override fun resolve(intent: Intent): OidcAction? { - return oidcUrlParser.parse(intent.dataString.orEmpty()) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt deleted file mode 100644 index 8933873dc2..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/OidcUrlParser.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.oidc.impl - -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider -import io.element.android.libraries.oidc.api.OidcAction - -fun interface OidcUrlParser { - fun parse(url: String): OidcAction? -} - -/** - * Simple parser for oidc url interception. - * TODO Find documentation about the format. - */ -@ContributesBinding(AppScope::class) -class DefaultOidcUrlParser( - private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, -) : OidcUrlParser { - /** - * Return a OidcAction, or null if the url is not a OidcUrl. - * Note: - * When user press button "Cancel", we get the url: - * `io.element.android:/?error=access_denied&state=IFF1UETGye2ZA8pO` - * On success, we get: - * `io.element.android:/?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB` - */ - override fun parse(url: String): OidcAction? { - if (url.startsWith(oidcRedirectUrlProvider.provide()).not()) return null - if (url.contains("error=access_denied")) return OidcAction.GoBack() - if (url.contains("code=")) return OidcAction.Success(url) - - // Other case not supported, let's crash the app for now - error("Not supported: $url") - } -} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt deleted file mode 100644 index 7f145c053d..0000000000 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcUrlParserTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.oidc.impl - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.auth.FAKE_REDIRECT_URL -import io.element.android.libraries.matrix.test.auth.FakeOidcRedirectUrlProvider -import io.element.android.libraries.oidc.api.OidcAction -import org.junit.Assert -import org.junit.Test - -class DefaultOidcUrlParserTest { - @Test - fun `test empty url`() { - val sut = createDefaultOidcUrlParser() - assertThat(sut.parse("")).isNull() - } - - @Test - fun `test regular url`() { - val sut = createDefaultOidcUrlParser() - assertThat(sut.parse("https://matrix.org")).isNull() - } - - @Test - fun `test cancel url`() { - val sut = createDefaultOidcUrlParser() - val aCancelUrl = "$FAKE_REDIRECT_URL?error=access_denied&state=IFF1UETGye2ZA8pO" - assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack()) - } - - @Test - fun `test success url`() { - val sut = createDefaultOidcUrlParser() - val aSuccessUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" - assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl)) - } - - @Test - fun `test unknown url`() { - val sut = createDefaultOidcUrlParser() - val anUnknownUrl = "$FAKE_REDIRECT_URL?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" - Assert.assertThrows(IllegalStateException::class.java) { - assertThat(sut.parse(anUnknownUrl)) - } - } - - private fun createDefaultOidcUrlParser(): DefaultOidcUrlParser { - return DefaultOidcUrlParser( - oidcRedirectUrlProvider = FakeOidcRedirectUrlProvider(), - ) - } -} diff --git a/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/customtab/FakeOidcActionFlow.kt b/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/customtab/FakeOidcActionFlow.kt deleted file mode 100644 index 5362aefa7c..0000000000 --- a/libraries/oidc/test/src/main/kotlin/io/element/android/libraries/oidc/test/customtab/FakeOidcActionFlow.kt +++ /dev/null @@ -1,33 +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.libraries.oidc.test.customtab - -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.libraries.oidc.api.OidcActionFlow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.MutableStateFlow - -/** - * This is actually a copy of DefaultOidcActionFlow. - */ -class FakeOidcActionFlow : OidcActionFlow { - private val mutableStateFlow = MutableStateFlow(null) - - override fun post(oidcAction: OidcAction) { - mutableStateFlow.value = oidcAction - } - - override suspend fun collect(collector: FlowCollector) { - mutableStateFlow.collect(collector) - } - - override fun reset() { - mutableStateFlow.value = null - } -} diff --git a/libraries/permissions/api/src/main/res/values-ca/translations.xml b/libraries/permissions/api/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..0b8527649b --- /dev/null +++ b/libraries/permissions/api/src/main/res/values-ca/translations.xml @@ -0,0 +1,7 @@ + + + "Per permetre que l\'aplicació utilitzi la càmera, concedeix-li permís a la configuració del sistema." + "Concedeix el permís a la configuració del sistema." + "Per permetre que l\'aplicació utilitzi el micròfon, concedeix-li permís a la configuració del sistema." + "Per permetre que l\'aplicació mostri notificacions, concedeix-li permís a la configuració del sistema." + diff --git a/libraries/permissions/impl/src/main/res/values-ca/translations.xml b/libraries/permissions/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..206c416d8b --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,5 @@ + + + "Comprova que l\'aplicació pot mostrar notificacions." + "Comprova els permisos" + diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts index a441616657..7084c960c6 100644 --- a/libraries/preferences/api/build.gradle.kts +++ b/libraries/preferences/api/build.gradle.kts @@ -9,7 +9,7 @@ import extension.testCommonDependencies */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt index 476658946a..b1877b3ae5 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt @@ -23,6 +23,9 @@ interface AppPreferencesStore { suspend fun setTheme(theme: String) fun getThemeFlow(): Flow + suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) + fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") suspend fun setHideInviteAvatars(hide: Boolean?) @Deprecated("Use MediaPreviewService instead. Kept only for migration.") @@ -38,5 +41,27 @@ interface AppPreferencesStore { suspend fun setTracingLogPacks(targets: Set) fun getTracingLogPacksFlow(): Flow> + fun getMessageSoundFlow(): Flow + + /** + * Atomically persists [sound] (with copy-time [title] for Custom; cleared otherwise) and + * bumps the channel version. Single transaction so process death can't desync URI and version. + */ + suspend fun setMessageSoundAndIncrementVersion(sound: NotificationSound, title: String?): Int + + /** Title captured at copy time. Null for SystemDefault / Silent or pre-title persisted data. */ + fun getMessageSoundDisplayNameFlow(): Flow + + fun getCallRingtoneFlow(): Flow + + /** See [setMessageSoundAndIncrementVersion]. */ + suspend fun setCallRingtoneAndIncrementVersion(sound: NotificationSound, title: String?): Int + + /** See [getMessageSoundDisplayNameFlow]. */ + fun getCallRingtoneDisplayNameFlow(): Flow + + /** Single-snapshot read of all sound prefs; used at boot to seed channels without N reads. */ + suspend fun getNotificationSoundChannelConfig(): NotificationSoundChannelConfig + suspend fun reset() } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/NotificationSound.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/NotificationSound.kt new file mode 100644 index 0000000000..f138dd592b --- /dev/null +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/NotificationSound.kt @@ -0,0 +1,77 @@ +/* + * 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.preferences.api.store + +import androidx.compose.runtime.Immutable + +/** + * The user's choice of notification sound for a notification channel. + */ +@Immutable +sealed interface NotificationSound { + /** Use the system's default tone for this channel (e.g. notification or ringtone). */ + data object SystemDefault : NotificationSound + + /** + * Use the bundled in-app sound. Semantically meaningful only for the noisy message channel, + * where it resolves to `R.raw.message`; the ringing-call channel has no bundled tone and + * treats this the same as [SystemDefault]. + */ + data object ElementDefault : NotificationSound + + /** + * Use the bundled in-app fade sound. Like [ElementDefault] this is meaningful only for the + * noisy message channel (`R.raw.element_fade`); the ringing-call channel has no bundled tone + * and treats this the same as [SystemDefault]. + */ + data object ElementFade : NotificationSound + + /** Produce no sound. */ + data object Silent : NotificationSound + + /** Use the ringtone at [uri]. */ + data class Custom(val uri: String) : NotificationSound + + companion object { + // String? round-trip used by [AppPreferencesStore]: + // null -> SystemDefault, "silent" -> Silent, "element_default" -> ElementDefault, + // "element_fade" -> ElementFade, else -> Custom(uri). + private const val STORED_SILENT = "silent" + private const val STORED_ELEMENT_DEFAULT = "element_default" + private const val STORED_ELEMENT_FADE = "element_fade" + + fun fromStored(value: String?): NotificationSound = when (value) { + null -> SystemDefault + STORED_SILENT -> Silent + STORED_ELEMENT_DEFAULT -> ElementDefault + STORED_ELEMENT_FADE -> ElementFade + else -> Custom(value) + } + + fun NotificationSound.toStored(): String? = when (this) { + SystemDefault -> null + Silent -> STORED_SILENT + ElementDefault -> STORED_ELEMENT_DEFAULT + ElementFade -> STORED_ELEMENT_FADE + is Custom -> uri + } + } +} + +/** + * Snapshot of the persisted notification-sound state, returned by + * [AppPreferencesStore.getNotificationSoundChannelConfig] in a single read. + */ +data class NotificationSoundChannelConfig( + val messageSound: NotificationSound, + val messageSoundVersion: Int, + val messageSoundDisplayName: String?, + val callRingtone: NotificationSound, + val callRingtoneVersion: Int, + val callRingtoneDisplayName: String?, +) diff --git a/libraries/preferences/impl/build.gradle.kts b/libraries/preferences/impl/build.gradle.kts index c567471da4..73327a69a2 100644 --- a/libraries/preferences/impl/build.gradle.kts +++ b/libraries/preferences/impl/build.gradle.kts @@ -1,4 +1,5 @@ import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright (c) 2025 Element Creations Ltd. @@ -25,4 +26,7 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.sessionStorage.api) + testCommonDependencies(libs) + testImplementation(projects.libraries.preferences.test) } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index 6856f8bdb6..af35ac3c44 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.preferences.impl.store import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding @@ -19,8 +20,12 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.preferences.api.store.NotificationSound.Companion.toStored +import io.element.android.libraries.preferences.api.store.NotificationSoundChannelConfig import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map private val developerModeKey = booleanPreferencesKey("developerMode") @@ -28,8 +33,15 @@ private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseU private val themeKey = stringPreferencesKey("theme") private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars") private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue") +private val liveLocationMinimumDistanceUpdateKey = intPreferencesKey("liveLocationMinimumDistanceUpdate") private val logLevelKey = stringPreferencesKey("logLevel") private val traceLogPacksKey = stringPreferencesKey("traceLogPacks") +private val messageSoundUriKey = stringPreferencesKey("notificationMessageSoundUri") +private val messageSoundChannelVersionKey = intPreferencesKey("notificationMessageSoundChannelVersion") +private val messageSoundDisplayNameKey = stringPreferencesKey("notificationMessageSoundDisplayName") +private val callRingtoneUriKey = stringPreferencesKey("notificationCallRingtoneUri") +private val callRingtoneChannelVersionKey = intPreferencesKey("notificationCallRingtoneChannelVersion") +private val callRingtoneDisplayNameKey = stringPreferencesKey("notificationCallRingtoneDisplayName") @ContributesBinding(AppScope::class) class DefaultAppPreferencesStore( @@ -79,6 +91,18 @@ class DefaultAppPreferencesStore( } } + override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) { + store.edit { prefs -> + prefs[liveLocationMinimumDistanceUpdateKey] = value + } + } + + override fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow { + return store.data.map { prefs -> + prefs[liveLocationMinimumDistanceUpdateKey] ?: 10 + } + } + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") override fun getHideInviteAvatarsFlow(): Flow { return store.data.map { prefs -> @@ -144,6 +168,75 @@ class DefaultAppPreferencesStore( } } + override fun getMessageSoundFlow(): Flow { + return store.data.map { prefs -> NotificationSound.fromStored(prefs[messageSoundUriKey]) } + } + + override suspend fun setMessageSoundAndIncrementVersion(sound: NotificationSound, title: String?): Int { + var newVersion = 0 + store.edit { prefs -> + val stored = sound.toStored() + if (stored != null) { + prefs[messageSoundUriKey] = stored + } else { + prefs.remove(messageSoundUriKey) + } + // Clear title on non-Custom so the picker doesn't show a stale label after a revert. + if (sound is NotificationSound.Custom && !title.isNullOrBlank()) { + prefs[messageSoundDisplayNameKey] = title + } else { + prefs.remove(messageSoundDisplayNameKey) + } + newVersion = (prefs[messageSoundChannelVersionKey] ?: 0) + 1 + prefs[messageSoundChannelVersionKey] = newVersion + } + return newVersion + } + + override fun getMessageSoundDisplayNameFlow(): Flow { + return store.data.map { prefs -> prefs[messageSoundDisplayNameKey] } + } + + override fun getCallRingtoneFlow(): Flow { + return store.data.map { prefs -> NotificationSound.fromStored(prefs[callRingtoneUriKey]) } + } + + override suspend fun setCallRingtoneAndIncrementVersion(sound: NotificationSound, title: String?): Int { + var newVersion = 0 + store.edit { prefs -> + val stored = sound.toStored() + if (stored != null) { + prefs[callRingtoneUriKey] = stored + } else { + prefs.remove(callRingtoneUriKey) + } + if (sound is NotificationSound.Custom && !title.isNullOrBlank()) { + prefs[callRingtoneDisplayNameKey] = title + } else { + prefs.remove(callRingtoneDisplayNameKey) + } + newVersion = (prefs[callRingtoneChannelVersionKey] ?: 0) + 1 + prefs[callRingtoneChannelVersionKey] = newVersion + } + return newVersion + } + + override fun getCallRingtoneDisplayNameFlow(): Flow { + return store.data.map { prefs -> prefs[callRingtoneDisplayNameKey] } + } + + override suspend fun getNotificationSoundChannelConfig(): NotificationSoundChannelConfig { + val prefs = store.data.first() + return NotificationSoundChannelConfig( + messageSound = NotificationSound.fromStored(prefs[messageSoundUriKey]), + messageSoundVersion = prefs[messageSoundChannelVersionKey] ?: 0, + messageSoundDisplayName = prefs[messageSoundDisplayNameKey], + callRingtone = NotificationSound.fromStored(prefs[callRingtoneUriKey]), + callRingtoneVersion = prefs[callRingtoneChannelVersionKey] ?: 0, + callRingtoneDisplayName = prefs[callRingtoneDisplayNameKey], + ) + } + override suspend fun reset() { store.edit { it.clear() } } diff --git a/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt b/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt new file mode 100644 index 0000000000..c52d1648ac --- /dev/null +++ b/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt @@ -0,0 +1,57 @@ +/* + * 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.preferences.impl.store + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultAppPreferencesStoreTest { + private val buildMeta = BuildMeta( + buildType = BuildType.DEBUG, + isDebuggable = true, + applicationName = "Element X", + productionApplicationName = "Element", + desktopApplicationName = "Element Desktop", + applicationId = "io.element.android", + isEnterpriseBuild = false, + lowPrivacyLoggingEnabled = false, + versionName = "1.0.0", + versionCode = 1, + gitRevision = "test", + gitBranchName = "test", + flavorDescription = "test", + flavorShortDescription = "test", + ) + + @Test + fun `live location minimum distance defaults to 10`() = runTest { + val store = DefaultAppPreferencesStore( + buildMeta = buildMeta, + preferenceDataStoreFactory = FakePreferenceDataStoreFactory(), + ) + + assertThat(store.getLiveLocationMinimumDistanceInMetersUpdateFlow().first()).isEqualTo(10) + } + + @Test + fun `live location minimum distance persists updates`() = runTest { + val store = DefaultAppPreferencesStore( + buildMeta = buildMeta, + preferenceDataStoreFactory = FakePreferenceDataStoreFactory(), + ) + + store.setLiveLocationMinimumDistanceInMetersUpdate(25) + + assertThat(store.getLiveLocationMinimumDistanceInMetersUpdateFlow().first()).isEqualTo(25) + } +} diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt index 6e7d22a568..6499e70223 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt @@ -12,8 +12,11 @@ import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.preferences.api.store.NotificationSoundChannelConfig import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.updateAndGet class InMemoryAppPreferencesStore( isDeveloperModeEnabled: Boolean = false, @@ -21,16 +24,30 @@ class InMemoryAppPreferencesStore( hideInviteAvatars: Boolean? = null, timelineMediaPreviewValue: MediaPreviewValue? = null, theme: String? = null, + liveLocationMinimumDistanceUpdate: Int = 10, logLevel: LogLevel = LogLevel.INFO, - traceLockPacks: Set = emptySet(), + traceLogPacks: Set = emptySet(), + messageSound: NotificationSound = NotificationSound.SystemDefault, + messageSoundChannelVersion: Int = 0, + messageSoundDisplayName: String? = null, + callRingtone: NotificationSound = NotificationSound.SystemDefault, + callRingtoneChannelVersion: Int = 0, + callRingtoneDisplayName: String? = null, ) : AppPreferencesStore { private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) private val theme = MutableStateFlow(theme) + private val liveLocationMinimumDistanceUpdate = MutableStateFlow(liveLocationMinimumDistanceUpdate) private val logLevel = MutableStateFlow(logLevel) - private val tracingLogPacks = MutableStateFlow(traceLockPacks) + private val tracingLogPacks = MutableStateFlow(traceLogPacks) private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars) private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue) + private val messageSound = MutableStateFlow(messageSound) + private val messageSoundChannelVersion = MutableStateFlow(messageSoundChannelVersion) + private val messageSoundDisplayName = MutableStateFlow(messageSoundDisplayName) + private val callRingtone = MutableStateFlow(callRingtone) + private val callRingtoneChannelVersion = MutableStateFlow(callRingtoneChannelVersion) + private val callRingtoneDisplayName = MutableStateFlow(callRingtoneDisplayName) override suspend fun setDeveloperModeEnabled(enabled: Boolean) { isDeveloperModeEnabled.value = enabled @@ -56,6 +73,14 @@ class InMemoryAppPreferencesStore( return theme } + override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) { + liveLocationMinimumDistanceUpdate.value = value + } + + override fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow { + return liveLocationMinimumDistanceUpdate + } + @Deprecated("Use MediaPreviewService instead. Kept only for migration.") override fun getHideInviteAvatarsFlow(): Flow { return hideInviteAvatars @@ -92,6 +117,45 @@ class InMemoryAppPreferencesStore( return tracingLogPacks } + override fun getMessageSoundFlow(): Flow { + return messageSound + } + + override suspend fun setMessageSoundAndIncrementVersion(sound: NotificationSound, title: String?): Int { + messageSound.value = sound + messageSoundDisplayName.value = if (sound is NotificationSound.Custom && !title.isNullOrBlank()) title else null + return messageSoundChannelVersion.updateAndGet { it + 1 } + } + + override fun getMessageSoundDisplayNameFlow(): Flow { + return messageSoundDisplayName + } + + override fun getCallRingtoneFlow(): Flow { + return callRingtone + } + + override suspend fun setCallRingtoneAndIncrementVersion(sound: NotificationSound, title: String?): Int { + callRingtone.value = sound + callRingtoneDisplayName.value = if (sound is NotificationSound.Custom && !title.isNullOrBlank()) title else null + return callRingtoneChannelVersion.updateAndGet { it + 1 } + } + + override fun getCallRingtoneDisplayNameFlow(): Flow { + return callRingtoneDisplayName + } + + override suspend fun getNotificationSoundChannelConfig(): NotificationSoundChannelConfig { + return NotificationSoundChannelConfig( + messageSound = messageSound.value, + messageSoundVersion = messageSoundChannelVersion.value, + messageSoundDisplayName = messageSoundDisplayName.value, + callRingtone = callRingtone.value, + callRingtoneVersion = callRingtoneChannelVersion.value, + callRingtoneDisplayName = callRingtoneDisplayName.value, + ) + } + override suspend fun reset() { // No op } diff --git a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt index 1b73ccea31..49f77ddefb 100644 --- a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt +++ b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt @@ -8,6 +8,16 @@ package io.element.android.libraries.previewutils.room +import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE +import io.element.android.libraries.designsystem.preview.USER_NAME_BOB +import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL +import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID +import io.element.android.libraries.designsystem.preview.USER_NAME_EVE +import io.element.android.libraries.designsystem.preview.USER_NAME_JUSTIN +import io.element.android.libraries.designsystem.preview.USER_NAME_MALLORY +import io.element.android.libraries.designsystem.preview.USER_NAME_SUSIE +import io.element.android.libraries.designsystem.preview.USER_NAME_VICTOR +import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER 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 @@ -23,6 +33,7 @@ fun aRoomMember( isIgnored: Boolean = false, role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, + isServiceMember: Boolean = false, ) = RoomMember( userId = userId, displayName = displayName, @@ -33,32 +44,33 @@ fun aRoomMember( isIgnored = isIgnored, role = role, membershipChangeReason = membershipChangeReason, + isServiceMember = isServiceMember, ) fun aRoomMemberList() = persistentListOf( anAlice(), aBob(), - aRoomMember(UserId("@carol:server.org"), "Carol"), - aRoomMember(UserId("@david:server.org"), "David"), - aRoomMember(UserId("@eve:server.org"), "Eve"), - aRoomMember(UserId("@justin:server.org"), "Justin"), - aRoomMember(UserId("@mallory:server.org"), "Mallory"), - aRoomMember(UserId("@susie:server.org"), "Susie"), + aRoomMember(UserId("@carol:server.org"), USER_NAME_CAROL), + aRoomMember(UserId("@david:server.org"), USER_NAME_DAVID), + aRoomMember(UserId("@eve:server.org"), USER_NAME_EVE), + aRoomMember(UserId("@justin:server.org"), USER_NAME_JUSTIN), + aRoomMember(UserId("@mallory:server.org"), USER_NAME_MALLORY), + aRoomMember(UserId("@susie:server.org"), USER_NAME_SUSIE), aVictor(), aWalter(), ) -fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice", role = RoomMember.Role.Admin) -fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob", role = RoomMember.Role.Moderator) +fun anAlice() = aRoomMember(UserId("@alice:server.org"), USER_NAME_ALICE, role = RoomMember.Role.Admin) +fun aBob() = aRoomMember(UserId("@bob:server.org"), USER_NAME_BOB, role = RoomMember.Role.Moderator) fun aVictor() = aRoomMember( UserId("@victor:server.org"), - "Victor", + USER_NAME_VICTOR, membership = RoomMembershipState.INVITE ) fun aWalter() = aRoomMember( UserId("@walter:server.org"), - "Walter", + USER_NAME_WALTER, membership = RoomMembershipState.INVITE ) diff --git a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.kt b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.kt index 4bf1d2501b..6f86789693 100644 --- a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.kt +++ b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/SpaceRoomFixture.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.previewutils.room +import io.element.android.libraries.designsystem.preview.SPACE_NAME 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.CurrentUserMembership @@ -19,7 +20,7 @@ import kotlinx.collections.immutable.toImmutableList fun aSpaceRoom( rawName: String? = null, - displayName: String = "Space name", + displayName: String = SPACE_NAME, avatarUrl: String? = null, canonicalAlias: RoomAlias? = null, childrenCount: Int = 0, @@ -33,6 +34,7 @@ fun aSpaceRoom( topic: String? = null, worldReadable: Boolean = false, isDirect: Boolean? = null, + isDm: Boolean? = null, via: List = emptyList(), ) = SpaceRoom( rawName = rawName, @@ -50,5 +52,6 @@ fun aSpaceRoom( topic = topic, worldReadable = worldReadable, via = via.toImmutableList(), - isDirect = isDirect + isDirect = isDirect, + isDm = isDm, ) diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts index df6ac6120a..91e545daed 100644 --- a/libraries/push/api/build.gradle.kts +++ b/libraries/push/api/build.gradle.kts @@ -20,5 +20,6 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixmedia.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.pushproviders.api) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt index ff7119b647..367052eaad 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationIdProvider.kt @@ -53,4 +53,5 @@ object NotificationIdProvider { enum class ForegroundServiceType { INCOMING_CALL, ONGOING_CALL, + LIVE_LOCATION, } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.kt new file mode 100644 index 0000000000..b23c4122ad --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.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.libraries.push.api.notifications + +import io.element.android.libraries.preferences.api.store.NotificationSound + +/** + * Reads and writes the sound on the message / ringing-call channels. Writes create a new + * versioned channel because Android forbids mutating sound after creation. + */ +interface NotificationSoundUpdater { + fun recreateNoisyChannel(sound: NotificationSound, version: Int) + + fun recreateRingingCallChannel(sound: NotificationSound, version: Int) + + /** Current channel sound classified into [NotificationSound]. Null when the channel doesn't exist. */ + suspend fun readNoisyChannelSound(): NotificationSound? + + /** See [readNoisyChannelSound]. */ + suspend fun readRingingCallChannelSound(): NotificationSound? +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/SoundDisplayNameResolver.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/SoundDisplayNameResolver.kt new file mode 100644 index 0000000000..a850b3e435 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/SoundDisplayNameResolver.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.push.api.notifications + +/** + * Resolves the user-facing title of a custom notification sound URI (the title shown by the + * system ringtone picker). Lifts the [android.media.RingtoneManager] side effect out of UI code + * so views and presenters stay testable. + */ +interface SoundDisplayNameResolver { + /** Returns the title of the ringtone at [uri], or null if it cannot be resolved. */ + suspend fun resolveCustomSoundTitle(uri: String): String? +} diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt index 504adacdb6..4ea80e4ce8 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt @@ -22,7 +22,7 @@ interface NotificationConversationService { suspend fun onSendMessage( sessionId: SessionId, roomId: RoomId, - roomName: String, + roomName: String?, roomIsDirect: Boolean, roomAvatarUrl: String?, ) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt new file mode 100644 index 0000000000..2268198353 --- /dev/null +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt @@ -0,0 +1,47 @@ +/* + * 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.push.api.notifications.sound + +/** + * Copies a picked notification sound into app-private storage and re-exposes it via the app's + * FileProvider, so the persisted reference outlives the original source. + */ +interface NotificationSoundCopier { + suspend fun copyToAppFiles(sourceUriString: String, slot: SoundSlot): CopyResult + + /** + * Removes any previously copied file for [slot] from app-private storage. No-op when no file + * exists. Call this when persisting a non-[NotificationSound.Custom] pick (SystemDefault / + * Silent) so the previous Custom copy doesn't linger as orphaned bytes — [copyToAppFiles] + * already sweeps stale files inline for Custom-to-Custom transitions. + */ + suspend fun deleteStoredSoundFor(slot: SoundSlot) + + enum class SoundSlot { Message, Call } + + sealed interface CopyResult { + /** + * @property fileProviderUriString FileProvider URI to persist as the channel sound. + * @property displayName [android.media.Ringtone.getTitle] for the source, or null. The + * picker substitutes a localised fallback label, so localisation stays in the UI layer. + */ + data class Success(val fileProviderUriString: String, val displayName: String?) : CopyResult + + /** [android.media.RingtoneManager] could not open the source URI as a Ringtone. */ + data object UnplayableSource : CopyResult + + /** The bytes copied successfully, but the resulting file is not a playable Ringtone. */ + data object UnplayableCopy : CopyResult + + /** Source exceeded the 5 MB cap. */ + data object FileTooLarge : CopyResult + + /** Any other I/O or unexpected failure. */ + data class Failure(val cause: Throwable) : CopyResult + } +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index e8acda59da..b9082c50f6 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.troubleshoot.api) implementation(projects.libraries.workmanager.api) + implementation(projects.features.announcement.api) implementation(projects.features.call.api) implementation(projects.features.enterprise.api) implementation(projects.features.lockscreen.api) @@ -80,6 +81,7 @@ dependencies { testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.troubleshoot.test) testImplementation(projects.libraries.workmanager.test) + testImplementation(projects.features.announcement.test) testImplementation(projects.features.call.test) testImplementation(projects.features.enterprise.test) testImplementation(projects.features.lockscreen.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt index 39f9d3acd6..35482aa164 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt @@ -83,7 +83,8 @@ class DefaultPusherSubscriber( // TODO getDeviceInfoUseCase.execute().displayName().orEmpty() deviceDisplayName = "MyDevice", url = gateway, - defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId)) + defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId)), + append = false, ) /** diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt index 6472883d4d..93194b2738 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt @@ -17,7 +17,9 @@ import dev.zacsweers.metro.SingleIn import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.push.impl.PushDatabase import io.element.encrypteddb.SqlCipherDriverFactory -import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider +import io.element.encrypteddb.passphrase.RandomDatabaseSecretProvider +import io.element.encrypteddb.utils.ReplaceDatabaseKey +import timber.log.Timber @BindingContainer @ContributesTo(AppScope::class) @@ -36,9 +38,20 @@ object PushHistoryModule { parentDir.mkdirs() } - val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile) + val rekeyMigrationVersion = 2L + val passphraseProvider = RandomDatabaseSecretProvider(context, secretFile) val driver = SqlCipherDriverFactory(passphraseProvider) - .create(PushDatabase.Schema, "$name.db", context) + .create( + schema = PushDatabase.Schema, + name = "$name.db", + context = context + ) { db, oldVersion, newVersion -> + Timber.d("Migrating $name database from version $oldVersion to $newVersion") + if (rekeyMigrationVersion in oldVersion..newVersion) { + ReplaceDatabaseKey(passphraseProvider).replaceKey(name, db) + } + } + return PushDatabase(driver) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index e328c46209..b6d15dac4e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.push.impl.notifications import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId @@ -37,6 +38,7 @@ import io.element.android.services.appnavstate.api.currentRoomId import io.element.android.services.appnavstate.api.currentSessionId import io.element.android.services.appnavstate.api.currentThreadId import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** @@ -55,6 +57,7 @@ class DefaultNotificationDrawerManager( private val matrixClientProvider: MatrixClientProvider, private val imageLoaderHolder: ImageLoaderHolder, private val activeNotificationsProvider: ActiveNotificationsProvider, + private val lockScreenService: LockScreenService, sessionObserver: SessionObserver, ) : NotificationCleaner { // TODO EAx add a setting per user for this @@ -197,6 +200,8 @@ class DefaultNotificationDrawerManager( it.sessionId } + val isAppLocked = lockScreenService.isPinSetup().first() + for ((sessionId, notifiableEvents) in eventsForSessions) { val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow() val imageLoader = imageLoaderHolder.get(client) @@ -207,7 +212,25 @@ class DefaultNotificationDrawerManager( } else { client.getUserProfile().getOrNull() ?: MatrixUser(sessionId) } - notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader) + if (isAppLocked) { + // When the app is locked, show a single fallback notification with event count + // instead of per-room notifications with message content/sender info. + clearAllMessagesEvents(sessionId) + val fallbackEvents = notifiableEvents.mapNotNull { it.toFallbackNotifiableEvent() } + notificationRenderer.render( + currentUser = currentUser, + useCompleteNotificationFormat = false, + eventsToProcess = fallbackEvents, + imageLoader = imageLoader, + ) + } else { + notificationRenderer.render( + currentUser = currentUser, + useCompleteNotificationFormat = useCompleteNotificationFormat, + eventsToProcess = notifiableEvents, + imageLoader = imageLoader, + ) + } } } } @@ -238,3 +261,29 @@ private fun AppNavigationState.shouldIgnoreEvent(event: NotifiableEvent): Boolea } } } + +/** + * Convert a [NotifiableEvent] into a [FallbackNotifiableEvent], stripping all content and sender info. + * Used when notification content should be hidden (app locked with PIN). + */ +private fun NotifiableEvent.toFallbackNotifiableEvent(): FallbackNotifiableEvent? { + val timestamp = when (this) { + is NotifiableMessageEvent -> timestamp + is InviteNotifiableEvent -> timestamp + is SimpleNotifiableEvent -> timestamp + is FallbackNotifiableEvent -> timestamp + is NotifiableRingingCallEvent -> return null + } + return FallbackNotifiableEvent( + sessionId = sessionId, + roomId = roomId, + eventId = eventId, + editedEventId = null, + description = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + timestamp = timestamp, + cause = null, + ) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSoundDisplayNameResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSoundDisplayNameResolver.kt new file mode 100644 index 0000000000..b6237aba03 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSoundDisplayNameResolver.kt @@ -0,0 +1,35 @@ +/* + * 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.push.impl.notifications + +import android.content.Context +import android.media.RingtoneManager +import androidx.core.net.toUri +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.push.api.notifications.SoundDisplayNameResolver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@ContributesBinding(AppScope::class) +class DefaultSoundDisplayNameResolver( + @ApplicationContext private val context: Context, +) : SoundDisplayNameResolver { + override suspend fun resolveCustomSoundTitle(uri: String): String? = withContext(Dispatchers.IO) { + val parsed = runCatchingExceptions { uri.toUri() }.getOrNull() ?: return@withContext null + // Probing our own FileProvider URI yields the internal filename ("call_sound.ogg"), + // not a user-meaningful tone name — better to fall through to the localised "Custom" + // label in the UI. + if (parsed.authority == context.notificationSoundFileProviderAuthority()) return@withContext null + runCatchingExceptions { + RingtoneManager.getRingtone(context, parsed)?.getTitle(context) + }.getOrNull() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index eb08a25c6a..cb9ef8c82d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -19,7 +19,6 @@ 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.CreateTimelineParams 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.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt index 0dd2761446..a97937f5d0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.push.impl.notifications import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.core.EventId @@ -215,9 +215,9 @@ class DefaultNotificationResultProcessor( private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) { Timber.i("## handleInternal() : Incoming call.") elementCallEntryPoint.handleIncomingCall( - callType = CallType.RoomCall( - notifiableEvent.sessionId, - notifiableEvent.roomId, + callData = CallData( + sessionId = notifiableEvent.sessionId, + roomId = notifiableEvent.roomId, isAudioCall = notifiableEvent.callIntent == CallIntent.AUDIO ), eventId = notifiableEvent.eventId, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationSoundFileProviderAuthority.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationSoundFileProviderAuthority.kt new file mode 100644 index 0000000000..db9fa47c02 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationSoundFileProviderAuthority.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.push.impl.notifications + +import android.content.Context + +// Mirrors the FileProvider authority declared in AndroidManifest.xml as +// `${applicationId}.fileprovider`. Kept here so the resolver's skip-rule and the copier's URI +// construction never drift apart. +internal const val NOTIFICATION_SOUND_FILE_PROVIDER_AUTHORITY_SUFFIX = ".fileprovider" + +internal fun Context.notificationSoundFileProviderAuthority(): String = + packageName + NOTIFICATION_SOUND_FILE_PROVIDER_AUTHORITY_SUFFIX diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 150f4a9a2d..d814fcaeb2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -67,7 +67,7 @@ class DefaultRoomGroupMessageCreator( val roomIsDm = !roomIsGroup return notificationCreator.createMessagesListNotification( notificationAccountParams = notificationAccountParams, - RoomEventGroupInfo( + roomInfo = RoomEventGroupInfo( sessionId = notificationAccountParams.user.userId, roomId = roomId, roomDisplayName = roomName, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index dc009c0b7d..8d9b776694 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.push.impl.notifications.channels import android.content.ContentResolver import android.content.Context +import android.content.Intent import android.media.AudioAttributes import android.media.AudioAttributes.USAGE_NOTIFICATION import android.media.AudioManager @@ -19,28 +20,50 @@ import android.provider.Settings import androidx.annotation.ChecksSdkIntAtLeast import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.net.toUri import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding import io.element.android.appconfig.NotificationConfig import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.push.api.notifications.NotificationSoundUpdater import io.element.android.libraries.push.impl.R import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import timber.log.Timber /* ========================================================================================== * IDs for channels * ========================================================================================== */ internal const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2" -internal const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_V2" +internal const val NOISY_NOTIFICATION_CHANNEL_ID_BASE = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_V2" internal const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V3" -internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID" +internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE = "RINGING_CALL_NOTIFICATION_CHANNEL_ID" + +private fun versionedChannelId(base: String, version: Int): String = + if (version <= 0) base else "${base}_v$version" + +internal fun noisyNotificationChannelId(version: Int): String = + versionedChannelId(NOISY_NOTIFICATION_CHANNEL_ID_BASE, version) + +internal fun ringingCallNotificationChannelId(version: Int): String = + versionedChannelId(RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE, version) /** * on devices >= android O, we need to define a channel for each notifications. + * + * Extends [NotificationSoundUpdater] so consumers can re-create the noisy and ringing-call + * channels when the user picks a new sound URI in settings. */ -interface NotificationChannels { +interface NotificationChannels : NotificationSoundUpdater { /** * Get the channel for incoming call. * @param ring true if the device should ring when receiving the call. @@ -66,14 +89,22 @@ interface NotificationChannels { private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O @SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) +@ContributesBinding(AppScope::class, binding = binding()) +@ContributesBinding(AppScope::class, binding = binding()) class DefaultNotificationChannels( private val notificationManager: NotificationManagerCompat, private val stringProvider: StringProvider, @ApplicationContext private val context: Context, private val enterpriseService: EnterpriseService, + private val appPreferencesStore: AppPreferencesStore, ) : NotificationChannels { + @Volatile private var currentNoisyChannelId: String = NOISY_NOTIFICATION_CHANNEL_ID_BASE + @Volatile private var currentRingingCallChannelId: String = RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE + + // Serializes concurrent recreate* calls; readers stay lock-free via @Volatile on the id fields. + private val recreateLock = Any() + init { createNotificationChannels() } @@ -88,9 +119,27 @@ class DefaultNotificationChannels( val accentColor = NotificationConfig.NOTIFICATION_ACCENT_COLOR + // Single-snapshot read; keep this path minimal — extra ContentResolver lookups here would + // risk a cold-start ANR. + val config = runBlocking { + val initial = appPreferencesStore.getNotificationSoundChannelConfig() + // Pre-dialog builds mapped the noisy `SystemDefault` to our bundled message.mp3. + // Now `SystemDefault` means Android's system tone, so untouched legacy state would + // silently flip sound. Promote it to `ElementDefault` once, gated on version==0 so the + // migration is idempotent. + if (initial.messageSoundVersion == 0 && initial.messageSound == NotificationSound.SystemDefault) { + appPreferencesStore.setMessageSoundAndIncrementVersion(NotificationSound.ElementDefault, title = null) + appPreferencesStore.getNotificationSoundChannelConfig() + } else { + initial + } + } + + currentNoisyChannelId = noisyNotificationChannelId(config.messageSoundVersion) + currentRingingCallChannelId = ringingCallNotificationChannelId(config.callRingtoneVersion) + // Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE // + currentTimeMillis). - // Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel // Starting from this version the channel will not be dynamic for (channel in notificationManager.notificationChannels) { val channelId = channel.id @@ -111,30 +160,19 @@ class DefaultNotificationChannels( notificationManager.deleteNotificationChannel(channelId) } } + // Drop older versioned channels; only the current one remains. + deleteStaleVersionedChannels(NOISY_NOTIFICATION_CHANNEL_ID_BASE, currentNoisyChannelId) + deleteStaleVersionedChannels(RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE, currentRingingCallChannelId) // Default notification importance: shows everywhere, makes noise, but does not visually intrude. + val noisySoundUri = resolveNoisySoundUri(config.messageSound) + grantSoundUriToSystem(noisySoundUri) notificationManager.createNotificationChannel( - NotificationChannelCompat.Builder( - NOISY_NOTIFICATION_CHANNEL_ID, - NotificationManagerCompat.IMPORTANCE_DEFAULT + buildNoisyChannel( + channelId = currentNoisyChannelId, + soundUri = noisySoundUri, + accentColor = accentColor, ) - .setSound( - Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - // Strangely we have to provide a "//" before the package name - .path("//" + context.packageName + "/" + R.raw.message) - .build(), - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(USAGE_NOTIFICATION) - .build(), - ) - .setName(stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" }) - .setDescription(stringProvider.getString(R.string.notification_channel_noisy)) - .setVibrationEnabled(true) - .setLightsEnabled(true) - .setLightColor(accentColor) - .build() ) // Low notification importance: shows everywhere, but is not intrusive. @@ -166,40 +204,205 @@ class DefaultNotificationChannels( ) // Register a channel for incoming call notifications which will ring the device when received + val ringingSoundUri = resolveRingingSoundUri(config.callRingtone) + grantSoundUriToSystem(ringingSoundUri) notificationManager.createNotificationChannel( - NotificationChannelCompat.Builder( - RINGING_CALL_NOTIFICATION_CHANNEL_ID, - NotificationManagerCompat.IMPORTANCE_MAX, + buildRingingCallChannel( + channelId = currentRingingCallChannelId, + soundUri = ringingSoundUri, + accentColor = accentColor, ) - .setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" }) - .setVibrationEnabled(true) - .setSound( - Settings.System.DEFAULT_RINGTONE_URI, - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setLegacyStreamType(AudioManager.STREAM_RING) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build() - ) - .setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls)) - .setLightsEnabled(true) - .setLightColor(accentColor) - .build() ) } + private fun buildNoisyChannel(channelId: String, soundUri: Uri?, accentColor: Int): NotificationChannelCompat { + val builder = NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_DEFAULT) + .setName(stringProvider.getString(R.string.notification_channel_noisy).ifEmpty { "Noisy notifications" }) + .setDescription(stringProvider.getString(R.string.notification_channel_noisy)) + .setVibrationEnabled(true) + .setLightsEnabled(true) + .setLightColor(accentColor) + if (soundUri != null) { + builder.setSound( + soundUri, + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(USAGE_NOTIFICATION) + .build(), + ) + } else { + builder.setSound(null, null) + } + return builder.build() + } + + private fun buildRingingCallChannel(channelId: String, soundUri: Uri?, accentColor: Int): NotificationChannelCompat { + val builder = NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_MAX) + .setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" }) + .setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls)) + .setVibrationEnabled(true) + .setLightsEnabled(true) + .setLightColor(accentColor) + if (soundUri != null) { + builder.setSound( + soundUri, + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setLegacyStreamType(AudioManager.STREAM_RING) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build() + ) + } else { + builder.setSound(null, null) + } + return builder.build() + } + + private fun resolveNoisySoundUri(sound: NotificationSound): Uri? = when (sound) { + NotificationSound.Silent -> null + NotificationSound.SystemDefault -> Settings.System.DEFAULT_NOTIFICATION_URI + NotificationSound.ElementDefault -> bundledMessageSoundUri() + NotificationSound.ElementFade -> bundledFadeSoundUri() + is NotificationSound.Custom -> parseUriOrFallback(sound.uri) { bundledMessageSoundUri() } + } + + private fun resolveRingingSoundUri(sound: NotificationSound): Uri? = when (sound) { + NotificationSound.Silent -> null + // The ringing channel has no bundled tone — treat ElementDefault and ElementFade like SystemDefault here. + NotificationSound.SystemDefault, + NotificationSound.ElementDefault, + NotificationSound.ElementFade -> Settings.System.DEFAULT_RINGTONE_URI + is NotificationSound.Custom -> parseUriOrFallback(sound.uri) { Settings.System.DEFAULT_RINGTONE_URI } + } + + private fun bundledMessageSoundUri(): Uri = + "${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context.packageName}/${R.raw.message}".toUri() + + private fun bundledFadeSoundUri(): Uri = + "${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context.packageName}/${R.raw.element_fade}".toUri() + + /** + * Lets system_server ("android") and SystemUI read our FileProvider sound URI; no-op otherwise. + * SystemUI hosts the lock-screen notification surface on most OEMs, so a missing grant there + * silently mutes ringtones when the device is locked. + */ + private fun grantSoundUriToSystem(uri: Uri?) { + if (uri == null || uri.scheme != ContentResolver.SCHEME_CONTENT) return + for (pkg in arrayOf("android", "com.android.systemui")) { + runCatchingExceptions { + context.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + }.onFailure { Timber.w(it, "grantUriPermission(%s) failed for notification sound", pkg) } + } + } + + /** Parses [uriString], or returns [fallback] (the SystemDefault URI) on failure. */ + private inline fun parseUriOrFallback(uriString: String, fallback: () -> Uri): Uri = + runCatchingExceptions { uriString.toUri() } + .getOrElse { + // Don't pass the throwable: legacy persisted URIs may carry SAF auth tokens. + Timber.w("Failed to parse persisted sound URI; falling back to default cause=%s", it::class.simpleName) + fallback() + } + + private fun deleteStaleVersionedChannels(baseId: String, currentId: String) { + if (!supportNotificationChannels()) return + for (channel in notificationManager.notificationChannels) { + val id = channel.id + // Match either the unversioned base ID or any "${baseId}_v" variant. + val isBaseOrVersioned = id == baseId || id.startsWith("${baseId}_v") + if (isBaseOrVersioned && id != currentId) { + notificationManager.deleteNotificationChannel(id) + } + } + } + override fun getChannelForIncomingCall(ring: Boolean): String { - return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID + return if (ring) currentRingingCallChannelId else CALL_NOTIFICATION_CHANNEL_ID } override fun getChannelIdForMessage(sessionId: SessionId, noisy: Boolean): String { return if (noisy) { enterpriseService.getNoisyNotificationChannelId(sessionId) - ?: NOISY_NOTIFICATION_CHANNEL_ID + ?: currentNoisyChannelId } else { SILENT_NOTIFICATION_CHANNEL_ID } } - override fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID + override fun getChannelIdForTest(): String = currentNoisyChannelId + + override fun recreateNoisyChannel(sound: NotificationSound, version: Int) { + if (!supportNotificationChannels()) return + synchronized(recreateLock) { + val accentColor = NotificationConfig.NOTIFICATION_ACCENT_COLOR + val newChannelId = noisyNotificationChannelId(version) + val soundUri = resolveNoisySoundUri(sound) + grantSoundUriToSystem(soundUri) + // Create channel before publishing the id: a reader landing between the assignment and + // the create call would notify() against a missing id, which Android silently drops. + notificationManager.createNotificationChannel( + buildNoisyChannel(newChannelId, soundUri, accentColor) + ) + currentNoisyChannelId = newChannelId + deleteStaleVersionedChannels(NOISY_NOTIFICATION_CHANNEL_ID_BASE, newChannelId) + } + } + + override fun recreateRingingCallChannel(sound: NotificationSound, version: Int) { + if (!supportNotificationChannels()) return + synchronized(recreateLock) { + val accentColor = NotificationConfig.NOTIFICATION_ACCENT_COLOR + val newChannelId = ringingCallNotificationChannelId(version) + val soundUri = resolveRingingSoundUri(sound) + grantSoundUriToSystem(soundUri) + // See recreateNoisyChannel: the channel must exist before the id is published. + notificationManager.createNotificationChannel( + buildRingingCallChannel(newChannelId, soundUri, accentColor) + ) + currentRingingCallChannelId = newChannelId + deleteStaleVersionedChannels(RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE, newChannelId) + } + } + + override suspend fun readNoisyChannelSound(): NotificationSound? { + return readChannelSound( + channelId = currentNoisyChannelId, + bundledUri = bundledMessageSoundUri(), + bundledFadeUri = bundledFadeSoundUri(), + systemDefaultUri = Settings.System.DEFAULT_NOTIFICATION_URI, + ) + } + + override suspend fun readRingingCallChannelSound(): NotificationSound? { + return readChannelSound( + channelId = currentRingingCallChannelId, + bundledUri = null, + bundledFadeUri = null, + systemDefaultUri = Settings.System.DEFAULT_RINGTONE_URI, + ) + } + + /** + * Classifies the channel's sound URI: null → Silent, bundled → ElementDefault, bundled fade → + * ElementFade, system default → SystemDefault, else → Custom. Both bundled URIs are nullable + * because the ringing channel has no bundled tones. + */ + private suspend fun readChannelSound( + channelId: String, + bundledUri: Uri?, + bundledFadeUri: Uri?, + systemDefaultUri: Uri, + ): NotificationSound? { + if (!supportNotificationChannels()) return null + val channel = withContext(Dispatchers.IO) { + notificationManager.getNotificationChannel(channelId) + } ?: return null + return when (val sound = channel.sound) { + null -> NotificationSound.Silent + bundledUri -> NotificationSound.ElementDefault + bundledFadeUri -> NotificationSound.ElementFade + systemDefaultUri -> NotificationSound.SystemDefault + else -> NotificationSound.Custom(sound.toString()) + } + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt index ce20234385..f898e4a9ed 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationService.kt @@ -76,7 +76,7 @@ class DefaultNotificationConversationService( override suspend fun onSendMessage( sessionId: SessionId, roomId: RoomId, - roomName: String, + roomName: String?, roomIsDirect: Boolean, roomAvatarUrl: String?, ) { @@ -93,10 +93,11 @@ class DefaultNotificationConversationService( val imageLoader = imageLoaderHolder.get(client) val defaultShortcutIconSize = ShortcutManagerCompat.getIconMaxWidth(context) + val name = roomName?.takeIf { it.isNotBlank() } ?: roomId.value val icon = bitmapLoader.getRoomBitmap( avatarData = AvatarData( id = roomId.value, - name = roomName, + name = name, url = roomAvatarUrl, size = AvatarSize.RoomDetailsHeader, ), @@ -105,7 +106,7 @@ class DefaultNotificationConversationService( )?.let(IconCompat::createWithBitmap) val shortcutInfo = ShortcutInfoCompat.Builder(context, createShortcutId(sessionId, roomId)) - .setShortLabel(roomName) + .setShortLabel(name) .setIcon(icon) .setIntent(intentProvider.getViewRoomIntent(sessionId, roomId, threadId = null, eventId = null)) .setCategories(categories) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/sound/DefaultNotificationSoundCopier.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/sound/DefaultNotificationSoundCopier.kt new file mode 100644 index 0000000000..2eeb07189a --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/sound/DefaultNotificationSoundCopier.kt @@ -0,0 +1,208 @@ +/* + * 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.push.impl.notifications.sound + +import android.content.ContentResolver +import android.content.Context +import android.media.RingtoneManager +import android.net.Uri +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier.CopyResult +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier.SoundSlot +import io.element.android.libraries.push.impl.notifications.notificationSoundFileProviderAuthority +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.io.InputStream + +private const val MAX_BYTES = 5L * 1024 * 1024 +private const val DIRECTORY_NAME = "notification_sounds" +private const val FALLBACK_EXTENSION = "bin" +private const val COPY_BUFFER_SIZE = 8 * 1024 +private const val MAX_DISPLAY_NAME_LENGTH = 256 + +// Defence-in-depth: we only ever expect URIs from the system ringtone picker (content://) or +// the bundled resource (android.resource://). Reject anything else before doing I/O so a future +// caller that constructs a NotificationSound.Custom from an untrusted intent can't trick the +// copier into reading file:// or unrelated providers. +private val ALLOWED_SOURCE_SCHEMES = setOf( + ContentResolver.SCHEME_CONTENT, + ContentResolver.SCHEME_ANDROID_RESOURCE, +) + +@ContributesBinding(AppScope::class) +class DefaultNotificationSoundCopier( + @ApplicationContext private val context: Context, +) : NotificationSoundCopier { + // Per-slot serialization: concurrent picks for the same slot would race on `.tmp`. + private val slotMutexes = SoundSlot.entries.associateWith { Mutex() } + + override suspend fun copyToAppFiles(sourceUriString: String, slot: SoundSlot): CopyResult = withContext(Dispatchers.IO) { + slotMutexes.getValue(slot).withLock { + runCatchingExceptions { performCopy(sourceUriString, slot) } + .getOrElse { ex -> + // Don't pass the throwable to Timber: SAF/MediaProvider exceptions sometimes + // embed the source URI (which can carry short-lived auth tokens). + Timber.w("Notification sound copy failed for slot=%s cause=%s", slot, ex::class.simpleName) + CopyResult.Failure(ex) + } + } + } + + override suspend fun deleteStoredSoundFor(slot: SoundSlot) { + withContext(Dispatchers.IO) { + slotMutexes.getValue(slot).withLock { + val dir = File(context.filesDir, DIRECTORY_NAME) + if (!dir.exists()) return@withLock + val slotBase = slotBaseFor(slot) + dir.listFiles { f -> f.isFile && f.name.startsWith("$slotBase.") } + ?.forEach { stale -> + if (!stale.delete()) { + Timber.w("Could not remove stored slot file %s", stale.name) + } + } + } + } + } + + private fun slotBaseFor(slot: SoundSlot): String = when (slot) { + SoundSlot.Message -> "message_sound" + SoundSlot.Call -> "call_sound" + } + + private fun performCopy(sourceUriString: String, slot: SoundSlot): CopyResult { + val source = sourceUriString.toUri() + if (source.scheme !in ALLOWED_SOURCE_SCHEMES) return CopyResult.UnplayableSource + // Reject unplayable sources up front instead of persisting a known-broken pick. + val sourceRingtone = RingtoneManager.getRingtone(context, source) + ?: return CopyResult.UnplayableSource + // Capture the source's display name before copy — once persisted, the row only sees the + // FileProvider URI, where Ringtone.getTitle() would resolve to our internal filename + // (e.g. "call_sound.ogg"). RingtoneManager → MediaStore TITLE → OpenableColumns covers + // system tones, MediaStore-indexed audio, and SAF-picked files respectively. The value + // crosses our security boundary (foreign provider → persisted state → UI), so cap and + // sanitize it. + val displayName = (sourceRingtone.getTitle(context)?.takeUnless { it.isBlank() } + ?: queryOpenableDisplayName(source)) + ?.let(::sanitizeDisplayName) + + // MIME comes from an arbitrary content provider; refuse extensions outside the platform + // table to keep filenames bounded to notification_sounds/. + val mimeMap = MimeTypeMap.getSingleton() + val candidateExtension = mimeMap.getExtensionFromMimeType(context.contentResolver.getType(source)) + val extension = candidateExtension + ?.takeIf { it.isNotBlank() && mimeMap.hasExtension(it) } + ?: FALLBACK_EXTENSION + + val slotBase = slotBaseFor(slot) + + val dir = File(context.filesDir, DIRECTORY_NAME).apply { mkdirs() } + val tmpFile = File(dir, "$slotBase.tmp").also { if (it.exists()) it.delete() } + val finalFile = File(dir, "$slotBase.$extension") + + val inputStream = context.contentResolver.openInputStream(source) + ?: return CopyResult.UnplayableSource + val bytesCopied = try { + inputStream.use { input -> tmpFile.outputStream().buffered().use { out -> input.copyToCapped(out, MAX_BYTES) } } + } catch (_: SizeLimitExceeded) { + tmpFile.delete() + Timber.w("Notification sound copy aborted: source exceeds %d bytes (slot=%s)", MAX_BYTES, slot) + return CopyResult.FileTooLarge + } + + // The finally below guarantees the temp file doesn't outlive a thrown probe / rename. + var committed = false + try { + // Probe through the FileProvider URI we'll actually persist (catches torn writes + // and OEM codec quirks). + val fileProviderUri = FileProvider.getUriForFile( + context, + context.notificationSoundFileProviderAuthority(), + tmpFile, + ) + if (RingtoneManager.getRingtone(context, fileProviderUri) == null) { + return CopyResult.UnplayableCopy + } + + // Sweep all `.*` so an extension change between picks doesn't leak orphans. + dir.listFiles { f -> f.isFile && f.name.startsWith("$slotBase.") && f.name != tmpFile.name } + ?.forEach { stale -> + if (!stale.delete()) { + Timber.w("Could not remove stale slot file %s", stale.name) + } + } + if (!tmpFile.renameTo(finalFile)) { + return CopyResult.Failure(IOException("Could not rename temp file to ${finalFile.name}")) + } + committed = true + + val finalUri = FileProvider.getUriForFile( + context, + context.notificationSoundFileProviderAuthority(), + finalFile, + ) + Timber.d("Notification sound copied: slot=%s bytes=%d", slot, bytesCopied) + return CopyResult.Success(finalUri.toString(), displayName) + } finally { + if (!committed) tmpFile.delete() + } + } + + private fun queryOpenableDisplayName(uri: Uri): String? { + return runCatchingExceptions { + context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null) + ?.use { cursor -> + if (cursor.moveToFirst() && cursor.columnCount > 0) { + cursor.getString(0)?.takeUnless { it.isBlank() } + } else { + null + } + } + }.getOrNull() + } +} + +internal fun sanitizeDisplayName(raw: String): String? { + // Display name comes from a foreign content provider; strip C0 / DEL control characters and + // cap length so a hostile or malformed value can't bloat persisted state or break UI rendering. + val cleaned = buildString(raw.length.coerceAtMost(MAX_DISPLAY_NAME_LENGTH)) { + for (ch in raw) { + if (length >= MAX_DISPLAY_NAME_LENGTH) break + if (ch.code >= 0x20 && ch.code != 0x7F) append(ch) + } + }.trim() + return cleaned.takeUnless { it.isEmpty() } +} + +private class SizeLimitExceeded : IOException("Sound source exceeds the configured size limit") + +private fun InputStream.copyToCapped(out: java.io.OutputStream, maxBytes: Long): Long { + val buffer = ByteArray(COPY_BUFFER_SIZE) + var totalBytes = 0L + while (true) { + val read = read(buffer) + if (read == -1) break + totalBytes += read + if (totalBytes > maxBytes) throw SizeLimitExceeded() + out.write(buffer, 0, read) + } + out.flush() + return totalBytes +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 44cf6edefc..d5c3f04348 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.push.impl.push import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.push.impl.db.PushRequest @@ -35,6 +36,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) @@ -53,6 +55,7 @@ class DefaultPushHandler( private val workManagerScheduler: WorkManagerScheduler, private val syncPendingNotificationsRequestFactory: SyncPendingNotificationsRequestBuilder.Factory, resultProcessor: NotificationResultProcessor, + private val dispatchers: CoroutineDispatchers, ) : PushHandler { init { resultProcessor.start() @@ -64,7 +67,7 @@ class DefaultPushHandler( * @param pushData the data received in the push. * @param providerInfo the provider info. */ - override suspend fun handle(pushData: PushData, providerInfo: String): Boolean { + override suspend fun handle(pushData: PushData, providerInfo: String): Boolean = withContext(dispatchers.computation) { // Start measuring how long it takes to display a notification from when the push is received Timber.d("Calculating push-to-notification for event ${pushData.eventId}") val parent = analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(pushData.eventId.value)) @@ -81,7 +84,7 @@ class DefaultPushHandler( } // Diagnostic Push - return if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { + if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { pushHistoryService.onDiagnosticPush(providerInfo) diagnosticPushHandler.handlePush() false @@ -90,7 +93,7 @@ class DefaultPushHandler( } } - override suspend fun handleInvalid(providerInfo: String, data: String) { + override suspend fun handleInvalid(providerInfo: String, data: String) = withContext(dispatchers.computation) { incrementPushDataStore.incrementPushCounter() pushHistoryService.onInvalidPushReceived(providerInfo, data) } 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 2b3587837e..b1c90a374a 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 @@ -13,17 +13,14 @@ import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import android.os.PowerManager +import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import androidx.core.app.ServiceCompat -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.impl.di.PushBindings -import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber @@ -34,6 +31,12 @@ private const val NOTIFICATION_ID = 1001 // This kind of foreground service can only last up to 3 minutes before onTimeout is called private val wakelockTimeout = 3.minutes.inWholeMilliseconds +// The channel ID to use for the notification of the foreground service. +private const val CHANNEL_ID = "fetch_push_notification_channel" + +// The tag to use for the wakelock, this is used for debugging purposes and should be unique to this service. +private const val WAKELOCK_TAG = "FetchPushService:WakeLock" + /** * Foreground service used to ensure the device stays awake while we handle the pushes and schedule and run the work to fetch the notification content. */ @@ -42,28 +45,35 @@ class FetchPushForegroundService : Service() { return null } - @Inject lateinit var notificationChannels: NotificationChannels - @Inject @AppCoroutineScope lateinit var coroutineScope: CoroutineScope - private val wakelock: PowerManager.WakeLock by lazy { val powerManager = getSystemService(POWER_SERVICE) as PowerManager - powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "FetchPushService:WakeLock").apply { + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { setReferenceCounted(false) } } private var isOnForeground = false + private fun ensureNotificationChannelExists() { + NotificationManagerCompat.from(this).createNotificationChannelsCompat( + listOf( + NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) + .setName(getString(CommonStrings.common_fetching_notifications_title_android).ifEmpty { "Syncing notifications…" }) + .setVibrationEnabled(false) + .setSound(null, null) + .build() + ) + ) + } + override fun onCreate() { - Timber.d("Creating FetchPushForegroundService") + Timber.i("Creating FetchPushForegroundService to handle incoming push, acquiring wakelock for up to $wakelockTimeout ms") + ensureNotificationChannelExists() - bindings().inject(this) - - Timber.d("Starting FetchPushForegroundService with wakelock timeout of $wakelockTimeout ms") // Start the foreground service as soon as possible - val notificationCompat = NotificationCompat.Builder(this, notificationChannels.getSilentChannelId()) + val notificationCompat = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(CommonDrawables.ic_notification) - .setContentTitle(getString(CommonStrings.common_android_fetching_notifications_title)) + .setContentTitle(getString(CommonStrings.common_fetching_notifications_title_android).ifEmpty { "Syncing notifications…" }) .setProgress(0, 0, true) .setVibrate(longArrayOf(0)) .setSound(null) @@ -103,7 +113,7 @@ class FetchPushForegroundService : Service() { // 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 { + MainScope().launch { delay(wakelockTimeout) onTimeoutAction(calledByTheSystem = false) } @@ -112,13 +122,18 @@ class FetchPushForegroundService : Service() { return START_NOT_STICKY } - override fun stopService(intent: Intent?): Boolean { - if (isOnForeground) { - wakelock.release() - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - } + override fun onDestroy() { + super.onDestroy() - return super.stopService(intent) + if (isOnForeground) { + Timber.i("Destroying FetchPushForegroundService, releasing wakelock and stopping foreground") + if (wakelock.isHeld) { + wakelock.release() + } + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + } else { + Timber.w("Destroying FetchPushForegroundService that was not running in foreground, this is unexpected") + } } override fun onTimeout(startId: Int) { @@ -127,9 +142,9 @@ class FetchPushForegroundService : Service() { } private fun onTimeoutAction(calledByTheSystem: Boolean) { - Timber.d("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") + Timber.w("onTimeoutAction, calledByTheSystem: $calledByTheSystem, isOnForeground: $isOnForeground") if (isOnForeground) { - Timber.d("Wakelock timeout reached, stopping FetchPushForegroundService") + Timber.w("Wakelock timeout reached, stopping FetchPushForegroundService") stopSelf() } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt index c18a0015aa..cefbd31515 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationsWorker.kt @@ -153,7 +153,7 @@ class FetchPendingNotificationsWorker( private suspend fun checkNetworkConnection(requests: List): Result? { val networkTimeoutSpans = requests.mapNotNull { request -> - val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(request.eventId)) + val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(request.eventId)) parent?.startChild("Waiting for network connectivity", "await_network") } diff --git a/libraries/push/impl/src/main/res/raw/element_fade.mp3 b/libraries/push/impl/src/main/res/raw/element_fade.mp3 new file mode 100644 index 0000000000..bf31420e49 Binary files /dev/null and b/libraries/push/impl/src/main/res/raw/element_fade.mp3 differ diff --git a/libraries/push/impl/src/main/res/values-ca/translations.xml b/libraries/push/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..56655d250d --- /dev/null +++ b/libraries/push/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,83 @@ + + + "Trucada" + "Escoltant esdeveniments" + "Notificacions sonores" + "Trucades amb so" + "Notificacions silencioses" + + "%1$s: %2$d missatge" + "%1$s: %2$d missatges" + + + "%d notificació" + "%d notificacions" + + "Tens missatges nous." + "📹 Trucada entrant" + "** No s\'ha pogut enviar. Obre la sala" + "Uneix-te" + "Rebutja" + + "%d invitació" + "%d invitacions" + + "T\'ha convidat a xatejar" + "%1$s t\'ha convidat a xatejar" + "T\'ha mencionat: %1$s" + "Missatges nous" + + "%d missatge nou" + "%d missatges nous" + + "Ha reaccionat amb %1$s" + "Marca com a llegit" + "Resposta ràpida" + "T\'ha convidat a unir-te a la sala" + "%1$s t\'ha convidat a unir-te a la sala" + "Jo" + "%1$s ha mencionat o respost" + "Estàs veient la notificació! Fes-hi clic!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + + "%d missatge notificat no llegit" + "%d missatges notificats no llegits" + + "%1$s i %2$s" + "%1$s en %2$s" + "%1$s en %2$s i %3$s" + + "%d sala" + "%d sales" + + "Sincronització en segon pla" + "Serveis de Google" + "No s\'ha trobat \'Serveis de Google Play\' vàlid. És possible que les notificacions no funcionin correctament." + "Usuaris bloquejats" + "Obté el nom del proveïdor actual." + "No s\'han seleccionat proveïdors push." + "Proveïdor actual de notificacions push: %1$s." + "Proveïdor actual de notificacions push" + "Assegura\'t que l\'aplicació admet com a mínim un proveïdor de notificacions push." + "No s\'ha trobat cap proveïdor de notificacions push." + + "S\'ha trobat %1$d proveïdor push: %2$s" + "S\'han trobat %1$d proveïdors push: %2$s" + + "L\'aplicació s\'ha creat per permetre: %1$s" + "Suport del proveïdor de notificacions Push" + "Comprova que l\'aplicació pot mostrar notificacions." + "No s\'ha fet clic a la notificació." + "No es pot mostrar la notificació." + "S\'ha fet clic a la notificació!" + "Mostrar notificació" + "Fes clic a la notificació per continuar el test." + "Assegureua\'t que l\'aplicació rep notificacions push." + "Error: el proveïdor push ha rebutjat la sol·licitud." + "Error: %1$s." + "Error, no s\'han pogut provar les notificacions push." + "Error, s\'ha acabat el temps d\'espera de la notificació push." + "El bucle de notificacions push ha tardat %1$d ms." + "Prova el bucle de notificacions push" + diff --git a/libraries/push/impl/src/main/res/values-et/translations.xml b/libraries/push/impl/src/main/res/values-et/translations.xml index 4b00b7b5e7..49010359cd 100644 --- a/libraries/push/impl/src/main/res/values-et/translations.xml +++ b/libraries/push/impl/src/main/res/values-et/translations.xml @@ -19,7 +19,8 @@ "Sul on %d uus sõnum." "Sul on %d uut sõnumit." - "📹 Sissetulev kõne" + "📞 Saabuv kõne" + "📹 Saabuv kõne" "** Saatmine ei õnnestunud - palun ava jututoa täisvaade" "Liitu" "Keeldu" diff --git a/libraries/push/impl/src/main/res/values-pl/translations.xml b/libraries/push/impl/src/main/res/values-pl/translations.xml index b9ae289507..d8a4077d05 100644 --- a/libraries/push/impl/src/main/res/values-pl/translations.xml +++ b/libraries/push/impl/src/main/res/values-pl/translations.xml @@ -15,7 +15,14 @@ "%d powiadomienia" "%d powiadomień" + "Nie udało się zarejestrować dystrybutora powiadomień Push, przez co nie będziesz już otrzymywać powiadomień. Sprawdź ustawienia powiadomień aplikacji i status dystrybutora powiadomień Push." "Masz nowe wiadomości." + + "Masz %d nową wiadomość." + "Masz %d nowe wiadomości." + "Masz %d nowych wiadomości." + + "📞 Połączenie przychodzące" "📹 Połączenie przychodzące" "** Nie udało się wysłać - proszę otworzyć pokój" "Dołącz" @@ -41,12 +48,15 @@ "%1$s zaprosił Cię do pokoju" "Ja" "%1$s wspomniał lub odpowiedział" + "Zaprosił Cię do dołączenia do przestrzeni" + "%1$s zaprosił Cię do dołączenia do przestrzeni" "Wyświetlasz powiadomienie! Kliknij mnie!" + "Wątek w %1$s" "%1$s: %2$s" "%1$s: %2$s %3$s" "%d nieprzeczytana wiadomość" - "%d nieprzeczytane wiadomość" + "%d nieprzeczytane wiadomości" "%d nieprzeczytanych wiadomości" "%1$s i %2$s" diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml index 0a83e6afad..2f677568ea 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -15,10 +15,16 @@ "Distribuitorul de notificări UnifiedPush nu a putut fi înregistrat, așadar nu veți mai primi notificări. Verificați setările de notificări ale aplicației și starea distribuitorului push." "Aveți mesaje noi" + + "Aveți %d mesaj nou." + "Aveți %d mesaje noi." + "Aveți %d mesaje noi." + + "📞 Apel primit" "Apel primit" "** Trimiterea eșuată - vă rugăm să deschideți camera" "Alăturați-vă" - "Respinge" + "Respingeți" "%d invitație" "%d invitații" 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 c0b597588b..2fe8645d82 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -15,8 +15,8 @@ "%d уведомления" "%d уведомлений" - "Не удалось зарегистрировать дистрибьютора уведомлений UnifiedPush, поэтому вы больше не будете получать уведомления. Проверьте настройки уведомлений в приложении и статус дистрибьютора push-уведомлений." - "У вас есть новые сообщения." + "Не удалось зарегистрировать дистрибьютора уведомлений UnifiedPush, поэтому Вы больше не будете получать уведомления. Проверьте настройки уведомлений в приложении и статус дистрибьютора push-уведомлений." + "У Вас есть новые сообщения." "У Вас %d новое сообщение." "У Вас %d новых сообщения." @@ -32,8 +32,8 @@ "%d приглашения" "%d приглашений" - "Пригласил(а) вас в чат" - "%1$s пригласил(а) вас в чат" + "Пригласил(а) Вас в чат" + "%1$s пригласил(а) Вас в чат" "Упомянул(а) вас: %1$s" "Новые сообщения" @@ -44,12 +44,12 @@ "Отреагировал(а) на %1$s" "Пометить как прочитанное" "Быстрый ответ" - "Пригласил(а) вас в комнату" - "%1$s пригласил(а) вас в комнату" + "Пригласил(а) Вас в комнату" + "%1$s пригласил(а) Вас в комнату" "Я" "%1$s упомянул(а) или ответил(а)" - "Пригласил(а) вас в пространство" - "%1$s пригласил(а) вас в пространство" + "Пригласил(а) Вас в пространство" + "%1$s пригласил(а) Вас в пространство" "Вы просматриваете уведомление! Нажмите на меня!" "Обсуждение в %1$s" "%1$s: %2$s" diff --git a/libraries/push/impl/src/main/res/values-uk/translations.xml b/libraries/push/impl/src/main/res/values-uk/translations.xml index d3aee22165..9c5f74605f 100644 --- a/libraries/push/impl/src/main/res/values-uk/translations.xml +++ b/libraries/push/impl/src/main/res/values-uk/translations.xml @@ -15,7 +15,14 @@ "%d сповіщення" "%d сповіщень" + "Розподільник сповіщень UnifiedPush не вдалося зареєструвати, тому ви більше не отримуватимете сповіщень. Перевірте налаштування сповіщень у додатку та статус розподільника push-сповіщень." "У вас є нові повідомлення." + + "У вас %d нове повідомлення." + "У вас %d нових повідомлення." + "У вас %d нових повідомлень." + + "📞 Вхідний дзвінок" "📹 Вхідний виклик" "** Не вдалося надіслати - відкрийте кімнату" "Доєднатися" @@ -41,6 +48,7 @@ "%1$s запросив вас приєднатися до кімнати" "Я" "%1$s згадували або відповідали" + "Вас запросили приєднатися до простору" "%1$s запрошує вас приєднатися до простору" "Ви переглядаєте сповіщення! Натисніть тут!" "Гілка в %1$s" @@ -62,11 +70,18 @@ "Фонова синхронізація" "Сервіси Google" "Не знайдено дійсних сервісів Google Play. Сповіщення можуть не працювати належним чином." + "Перевірка заблокованих користувачів" "Переглянути заблокованих користувачів" "Немає заблокованих користувачів" + + "Ви заблокували %1$d користувача. Ви не отримуватимете сповіщення для цього користувача." + "Ви заблокували %1$d користувачів. Ви не отримуватимете сповіщення для цих користувачів." + "Ви заблокували %1$d користувачів. Ви не отримуватимете сповіщення для цих користувачів." + "Заблоковані користувачі" "Отримує назву поточного постачальника." "Постачальників push-сповіщень не вибрано." + "Поточний провайдер push-повідомлень: %1$s та поточний дистриб\'ютор: %2$s. Але дистриб\'ютор %3$s не знайдено. Можливо, додаток було видалено?" "Поточний постачальник push-сповіщень: %1$s, але дистриб\'юторів не налаштовано." "Поточний постачальник: %1$s." "Поточний постачальник push-сповіщень: %1$s (%2$s)" 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 21988770c0..7532f16ba0 100644 --- a/libraries/push/impl/src/main/res/values-zh/translations.xml +++ b/libraries/push/impl/src/main/res/values-zh/translations.xml @@ -14,7 +14,7 @@ "UnifiedPush 分发器注册失败,你将无法再接收通知。请检查 app 的通知设置及推送分发器的状态。" "你有新消息。" - "你有 %d 条新消息。" + "你有 %d 个新消息。" "📞 来电" "📹 来电" @@ -65,7 +65,7 @@ "已屏蔽用户" "获取当前推送提供者的名称。" "未选择任何推送提供者。" - "当前推送提供这:%1$s 及当前分发器:%2$s . 但未找到分发器 %3$s。该 app 可能已被卸载?" + "当前推送提供者:%1$s 及当前分发器:%2$s。但未找到分发器 %3$s。该 app 可能已被卸载?" "当前推送提供者:%1$s ,但尚未配置分发器。" "当前推送提供者:%1$s。" "当前推送提供者:%1$s(%2$s)" diff --git a/libraries/push/impl/src/main/sqldelight/migrations/2.sqm b/libraries/push/impl/src/main/sqldelight/migrations/2.sqm new file mode 100644 index 0000000000..e7adfb091a --- /dev/null +++ b/libraries/push/impl/src/main/sqldelight/migrations/2.sqm @@ -0,0 +1,2 @@ +-- Migrate DB from version 2 +-- Dummy migration to trigger a re-key diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt index dd2cb24841..8cb1e2a7f6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt @@ -103,6 +103,7 @@ class DefaultPusherSubscriberTest { profileTag = DEFAULT_PUSHER_FILE_TAG + "_", lang = "en", defaultPayload = "{\"cs\":\"$A_SECRET\"}", + append = false, ), ) ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt index dd1dd7b74d..b3358ba97f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/AndroidBatteryOptimizationTest.kt @@ -16,12 +16,10 @@ import com.google.common.truth.Truth.assertThat import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class AndroidBatteryOptimizationTest { +class AndroidBatteryOptimizationTest : RobolectricTest() { @Test fun `isIgnoringBatteryOptimizations should return false`() { val sut = createAndroidBatteryOptimization() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt index 04a65073f6..df9697b94f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt @@ -19,14 +19,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.mockk.every import io.mockk.mockk import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultActiveNotificationsProviderTest { +class DefaultActiveNotificationsProviderTest : RobolectricTest() { private val notificationIdProvider = NotificationIdProvider @Test diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt index 3caec04add..408063712a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt @@ -31,18 +31,16 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment private const val A_ROOM_AVATAR = "mxc://roomAvatar" private const val A_USER_AVATAR_1 = "mxc://userAvatar1" private const val A_USER_AVATAR_2 = "mxc://userAvatar2" -@RunWith(RobolectricTestRunner::class) -class DefaultBaseRoomGroupMessageCreatorTest { +class DefaultBaseRoomGroupMessageCreatorTest : RobolectricTest() { @Test fun `test createRoomMessage with one Event`() = runTest { val sut = createRoomGroupMessageCreator() 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 33634e1767..e196ddb0ec 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 @@ -59,16 +59,14 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv 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 +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config @Suppress("LargeClass") -@RunWith(RobolectricTestRunner::class) -class DefaultNotifiableEventResolverTest { +class DefaultNotifiableEventResolverTest : RobolectricTest() { @Test fun `resolve event no session`() = runTest { val sut = createDefaultNotifiableEventResolver(notificationService = null) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index 04da6ef556..4e2685e63a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -13,6 +13,8 @@ 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.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 @@ -510,6 +512,7 @@ fun TestScope.createDefaultNotificationDrawerManager( enterpriseService: EnterpriseService = FakeEnterpriseService(), sessionObserver: SessionObserver = FakeSessionObserver(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), + lockScreenService: LockScreenService = FakeLockScreenService(), ): DefaultNotificationDrawerManager { return DefaultNotificationDrawerManager( notificationDisplayer = notificationDisplayer, @@ -530,6 +533,7 @@ fun TestScope.createDefaultNotificationDrawerManager( matrixClientProvider = matrixClientProvider, imageLoaderHolder = FakeImageLoaderHolder(), activeNotificationsProvider = activeNotificationsProvider, + lockScreenService = lockScreenService, sessionObserver = sessionObserver, ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt index 6acb375bac..91f29dd28e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.push.impl.notifications import com.google.common.truth.Truth.assertThat -import io.element.android.features.call.api.CallType +import io.element.android.features.call.api.CallData import io.element.android.features.call.test.FakeElementCallEntryPoint import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -104,7 +104,7 @@ class DefaultNotificationResultProcessorTest { @Test fun `when ringing call PushData is received, the incoming call will be handled`() = runTest { val handleIncomingCallLambda = lambdaRecorder< - CallType.RoomCall, + CallData, EventId, UserId, String?, @@ -140,7 +140,7 @@ class DefaultNotificationResultProcessorTest { fun `when notify call PushData is received, the incoming call will be treated as a normal notification`() = runTest { val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val handleIncomingCallLambda = lambdaRecorder< - CallType.RoomCall, + CallData, EventId, UserId, String?, @@ -176,7 +176,7 @@ class DefaultNotificationResultProcessorTest { fun `when notify call PushData is received, the incoming call will be treated as a normal notification even if notification are disabled`() = runTest { val onNotifiableEventsReceived = lambdaRecorder, Unit> {} val handleIncomingCallLambda = lambdaRecorder< - CallType.RoomCall, + CallData, EventId, UserId, String?, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt index 0973ee69ce..f623214fc4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt @@ -18,13 +18,11 @@ import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.nonNull +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultSummaryGroupMessageCreatorTest { +class DefaultSummaryGroupMessageCreatorTest : RobolectricTest() { @Test fun `process notifications`() = runTest { val notificationCreator = FakeNotificationCreator() 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 2cf666d92a..fa77e2d761 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 @@ -49,6 +49,7 @@ import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -56,12 +57,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - @ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -class NotificationBroadcastReceiverHandlerTest { +class NotificationBroadcastReceiverHandlerTest : RobolectricTest() { private val actionIds = NotificationActionIds(aBuildMeta()) @Test diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt index f897388c66..1a46546db8 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt @@ -24,10 +24,9 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNo import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner private val MY_AVATAR_URL: String? = null @@ -36,8 +35,7 @@ private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID) private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) private val A_FALLBACK_EVENT = aFallbackNotifiableEvent() -@RunWith(RobolectricTestRunner::class) -class NotificationDataFactoryTest { +class NotificationDataFactoryTest : RobolectricTest() { private val notificationCreator = FakeNotificationCreator() private val fakeRoomGroupMessageCreator = FakeRoomGroupMessageCreator() private val fakeSummaryGroupMessageCreator = FakeSummaryGroupMessageCreator() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 6ca19b7acc..43991444fe 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -32,10 +32,9 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner private const val MY_USER_DISPLAY_NAME = "display-name" private const val MY_USER_AVATAR_URL = "avatar-url" @@ -45,8 +44,7 @@ private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION) private val ONE_SHOT_NOTIFICATION = OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", isNoisy = false, timestamp = -1) -@RunWith(RobolectricTestRunner::class) -class NotificationRendererTest { +class NotificationRendererTest : RobolectricTest() { private val notificationDisplayer = FakeNotificationDisplayer() private val notificationCreator = FakeNotificationCreator() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt index 194caf7287..94d7905f6a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/FakeNotificationChannels.kt @@ -9,11 +9,19 @@ package io.element.android.libraries.push.impl.notifications.channels import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.tests.testutils.lambda.lambdaError class FakeNotificationChannels( var channelForIncomingCall: (ring: Boolean) -> String = { _ -> "" }, var channelIdForMessage: (sessionId: SessionId, noisy: Boolean) -> String = { _, _ -> "" }, - var channelIdForTest: () -> String = { "" } + var channelIdForTest: () -> String = { "" }, + // Side-effecting recreates default to lambdaError (matching FakeNotificationSoundUpdater) so a + // test that doesn't expect a channel rebuild loudly fails instead of silently swallowing the call. + var recreateNoisyChannelLambda: (sound: NotificationSound, version: Int) -> Unit = { _, _ -> lambdaError() }, + var recreateRingingCallChannelLambda: (sound: NotificationSound, version: Int) -> Unit = { _, _ -> lambdaError() }, + var readNoisyChannelSoundLambda: () -> NotificationSound? = { null }, + var readRingingCallChannelSoundLambda: () -> NotificationSound? = { null }, ) : NotificationChannels { override fun getChannelForIncomingCall(ring: Boolean): String { return channelForIncomingCall(ring) @@ -26,4 +34,16 @@ class FakeNotificationChannels( override fun getChannelIdForTest(): String { return channelIdForTest() } + + override fun recreateNoisyChannel(sound: NotificationSound, version: Int) { + recreateNoisyChannelLambda(sound, version) + } + + override fun recreateRingingCallChannel(sound: NotificationSound, version: Int) { + recreateRingingCallChannelLambda(sound, version) + } + + override suspend fun readNoisyChannelSound(): NotificationSound? = readNoisyChannelSoundLambda() + + override suspend fun readRingingCallChannelSound(): NotificationSound? = readRingingCallChannelSoundLambda() } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt index b004c35537..1df87232f5 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannelsTest.kt @@ -9,24 +9,29 @@ package io.element.android.libraries.push.impl.notifications.channels import android.os.Build +import android.provider.Settings import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.net.toUri 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.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.mockk.every import io.mockk.mockk +import io.mockk.slot import io.mockk.verify +import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -@RunWith(RobolectricTestRunner::class) -class NotificationChannelsTest { +class NotificationChannelsTest : RobolectricTest() { @Test @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) fun `init - creates notification channels and migrates old ones`() { @@ -45,7 +50,7 @@ class NotificationChannelsTest { val notificationChannels = createNotificationChannels() val ringingChannel = notificationChannels.getChannelForIncomingCall(ring = true) - assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID) + assertThat(ringingChannel).isEqualTo(RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE) val normalChannel = notificationChannels.getChannelForIncomingCall(ring = false) assertThat(normalChannel).isEqualTo(CALL_NOTIFICATION_CHANNEL_ID) @@ -59,7 +64,7 @@ class NotificationChannelsTest { ), ) assertThat(notificationChannels.getChannelIdForMessage(sessionId = A_SESSION_ID, noisy = true)) - .isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID) + .isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID_BASE) assertThat(notificationChannels.getChannelIdForMessage(sessionId = A_SESSION_ID, noisy = false)) .isEqualTo(SILENT_NOTIFICATION_CHANNEL_ID) } @@ -81,16 +86,418 @@ class NotificationChannelsTest { fun `getChannelIdForTest - returns the right channel`() { val notificationChannels = createNotificationChannels() - assertThat(notificationChannels.getChannelIdForTest()).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID) + assertThat(notificationChannels.getChannelIdForTest()).isEqualTo(NOISY_NOTIFICATION_CHANNEL_ID_BASE) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `init - reads persisted message version and seeds versioned channel id`() { + val notificationChannels = createNotificationChannels( + appPreferencesStore = InMemoryAppPreferencesStore(messageSoundChannelVersion = 3), + enterpriseService = FakeEnterpriseService(getNoisyNotificationChannelIdResult = { null }), + ) + assertThat(notificationChannels.getChannelIdForTest()).isEqualTo("${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v3") + assertThat(notificationChannels.getChannelIdForMessage(sessionId = A_SESSION_ID, noisy = true)) + .isEqualTo("${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v3") + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `init - reads persisted call ringtone version and seeds versioned channel id`() { + val notificationChannels = createNotificationChannels( + appPreferencesStore = InMemoryAppPreferencesStore(callRingtoneChannelVersion = 2), + ) + assertThat(notificationChannels.getChannelForIncomingCall(ring = true)) + .isEqualTo("${RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE}_v2") + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateNoisyChannel - creates new versioned channel and updates current id`() { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.Custom("content://media/internal/audio/media/42"), version = 7) + + val expectedId = "${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v7" + assertThat(channels.getChannelIdForTest()).isEqualTo(expectedId) + verify { notificationManager.createNotificationChannel(match { it.id == expectedId }) } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateNoisyChannel - Silent produces channel with no sound`() { + val captured = slot() + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { createNotificationChannel(capture(captured)) } returns Unit + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.Silent, version = 1) + + // The last captured channel (the recreated one) should have a null sound and the new id. + assertThat(captured.captured.id).isEqualTo("${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v1") + assertThat(captured.captured.sound).isNull() + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateNoisyChannel - SystemDefault uses Android system notification URI`() { + val captured = slot() + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { createNotificationChannel(capture(captured)) } returns Unit + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.SystemDefault, version = 2) + + assertThat(captured.captured.sound).isEqualTo(Settings.System.DEFAULT_NOTIFICATION_URI) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateNoisyChannel - ElementDefault uses bundled message URI`() { + val captured = slot() + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { createNotificationChannel(capture(captured)) } returns Unit + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.ElementDefault, version = 1) + + assertThat(captured.captured.sound?.scheme).isEqualTo("android.resource") + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateNoisyChannel - ElementFade uses bundled fade URI`() { + val captured = slot() + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { createNotificationChannel(capture(captured)) } returns Unit + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.ElementFade, version = 1) + + val context = RuntimeEnvironment.getApplication() + val expected = "android.resource://${context.packageName}/${io.element.android.libraries.push.impl.R.raw.element_fade}".toUri() + assertThat(captured.captured.sound).isEqualTo(expected) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateNoisyChannel - deletes prior versioned channels`() { + val priorChannel = mockk(relaxed = true) { + every { id } returns "${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v3" + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns listOf(priorChannel) + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.SystemDefault, version = 4) + + verify { notificationManager.deleteNotificationChannel("${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v3") } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateRingingCallChannel - creates new versioned channel and updates current id`() { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateRingingCallChannel(sound = NotificationSound.Custom("content://settings/system/ringtone"), version = 5) + + val expectedId = "${RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE}_v5" + assertThat(channels.getChannelForIncomingCall(ring = true)).isEqualTo(expectedId) + verify { notificationManager.createNotificationChannel(match { it.id == expectedId }) } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreateRingingCallChannel - Silent produces channel with no sound`() { + val captured = slot() + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { createNotificationChannel(capture(captured)) } returns Unit + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateRingingCallChannel(sound = NotificationSound.Silent, version = 1) + + assertThat(captured.captured.id).isEqualTo("${RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE}_v1") + assertThat(captured.captured.sound).isNull() + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `recreate noisy then ringing produces both versioned channels with current ids`() { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + channels.recreateNoisyChannel(sound = NotificationSound.Custom("content://a"), version = 5) + channels.recreateRingingCallChannel(sound = NotificationSound.Custom("content://b"), version = 7) + + // Each recreate updates the corresponding currentId independently — the lock guarding + // recreate* against itself does not interfere across the two channel families. + assertThat(channels.getChannelIdForTest()).isEqualTo("${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v5") + assertThat(channels.getChannelForIncomingCall(ring = true)).isEqualTo("${RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE}_v7") + verify { notificationManager.createNotificationChannel(match { it.id == "${NOISY_NOTIFICATION_CHANNEL_ID_BASE}_v5" }) } + verify { + notificationManager.createNotificationChannel( + match { it.id == "${RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE}_v7" } + ) + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readNoisyChannelSound - returns Custom for non-default URI`() = runTest { + val customUri = "content://media/internal/audio/media/42".toUri() + val channel = mockk(relaxed = true) { + every { sound } returns customUri + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readNoisyChannelSound() + + assertThat(result).isEqualTo(NotificationSound.Custom(customUri.toString())) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readNoisyChannelSound - classifies the bundled message URI as ElementDefault`() = runTest { + val context = RuntimeEnvironment.getApplication() + val ourBundled = "android.resource://${context.packageName}/${io.element.android.libraries.push.impl.R.raw.message}".toUri() + val channel = mockk(relaxed = true) { + every { sound } returns ourBundled + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readNoisyChannelSound() + + assertThat(result).isEqualTo(NotificationSound.ElementDefault) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readNoisyChannelSound - classifies the bundled fade URI as ElementFade`() = runTest { + val context = RuntimeEnvironment.getApplication() + val ourFade = "android.resource://${context.packageName}/${io.element.android.libraries.push.impl.R.raw.element_fade}".toUri() + val channel = mockk(relaxed = true) { + every { sound } returns ourFade + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readNoisyChannelSound() + + assertThat(result).isEqualTo(NotificationSound.ElementFade) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readNoisyChannelSound - classifies DEFAULT_NOTIFICATION_URI as SystemDefault`() = runTest { + val channel = mockk(relaxed = true) { + every { sound } returns Settings.System.DEFAULT_NOTIFICATION_URI + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readNoisyChannelSound() + + assertThat(result).isEqualTo(NotificationSound.SystemDefault) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `init - migrates legacy SystemDefault message sound to ElementDefault when version is 0`() = runTest { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.SystemDefault, + messageSoundChannelVersion = 0, + ) + + createNotificationChannels( + notificationManager = notificationManager, + appPreferencesStore = appPreferencesStore, + ) + + val config = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(config.messageSound).isEqualTo(NotificationSound.ElementDefault) + assertThat(config.messageSoundVersion).isEqualTo(1) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `init - migration is idempotent across multiple instantiations`() = runTest { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.SystemDefault, + messageSoundChannelVersion = 0, + ) + + // First boot: migrate. + createNotificationChannels(notificationManager = notificationManager, appPreferencesStore = appPreferencesStore) + val afterFirst = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(afterFirst.messageSound).isEqualTo(NotificationSound.ElementDefault) + assertThat(afterFirst.messageSoundVersion).isEqualTo(1) + + // Second boot: gate (version == 0) no longer matches, so the version must not bump again. + createNotificationChannels(notificationManager = notificationManager, appPreferencesStore = appPreferencesStore) + val afterSecond = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(afterSecond.messageSound).isEqualTo(NotificationSound.ElementDefault) + assertThat(afterSecond.messageSoundVersion).isEqualTo(1) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `init - does not re-migrate when message version is non-zero`() = runTest { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.SystemDefault, + messageSoundChannelVersion = 5, + ) + + createNotificationChannels( + notificationManager = notificationManager, + appPreferencesStore = appPreferencesStore, + ) + + val config = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(config.messageSound).isEqualTo(NotificationSound.SystemDefault) + assertThat(config.messageSoundVersion).isEqualTo(5) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `init - does not migrate when persisted message sound is not SystemDefault`() = runTest { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + } + val appPreferencesStore = InMemoryAppPreferencesStore( + messageSound = NotificationSound.Silent, + messageSoundChannelVersion = 0, + ) + + createNotificationChannels( + notificationManager = notificationManager, + appPreferencesStore = appPreferencesStore, + ) + + val config = appPreferencesStore.getNotificationSoundChannelConfig() + assertThat(config.messageSound).isEqualTo(NotificationSound.Silent) + assertThat(config.messageSoundVersion).isEqualTo(0) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readNoisyChannelSound - returns Silent when channel sound is null`() = runTest { + val channel = mockk(relaxed = true) { + every { sound } returns null + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readNoisyChannelSound() + + assertThat(result).isEqualTo(NotificationSound.Silent) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readNoisyChannelSound - returns null when channel does not exist`() = runTest { + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID_BASE) } returns null + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readNoisyChannelSound() + + assertThat(result).isNull() + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readRingingCallChannelSound - returns SystemDefault when channel sound matches DEFAULT_RINGTONE_URI`() = runTest { + val channel = mockk(relaxed = true) { + every { sound } returns Settings.System.DEFAULT_RINGTONE_URI + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readRingingCallChannelSound() + + assertThat(result).isEqualTo(NotificationSound.SystemDefault) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun `readRingingCallChannelSound - returns Custom for non-default URI`() = runTest { + val customUri = "content://media/external/audio/media/9001".toUri() + val channel = mockk(relaxed = true) { + every { sound } returns customUri + } + val notificationManager = mockk(relaxed = true) { + every { notificationChannels } returns emptyList() + every { getNotificationChannel(RINGING_CALL_NOTIFICATION_CHANNEL_ID_BASE) } returns channel + } + val channels = createNotificationChannels(notificationManager = notificationManager) + + val result = channels.readRingingCallChannelSound() + + assertThat(result).isEqualTo(NotificationSound.Custom(customUri.toString())) } private fun createNotificationChannels( notificationManager: NotificationManagerCompat = mockk(relaxed = true), enterpriseService: EnterpriseService = FakeEnterpriseService(), + // Default to ElementDefault so the legacy `(SystemDefault, version=0)` migration gate + // doesn't fire and silently rewrite the prefs store. Tests that exercise the migration + // construct their own store explicitly. + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(messageSound = NotificationSound.ElementDefault), ) = DefaultNotificationChannels( notificationManager = notificationManager, stringProvider = FakeStringProvider(), context = RuntimeEnvironment.getApplication(), enterpriseService = enterpriseService, + appPreferencesStore = appPreferencesStore, ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt index d4d7713c2d..41e601c50d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt @@ -26,18 +26,15 @@ import io.element.android.libraries.push.impl.notifications.factories.FakeIntent import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver +import io.element.android.tests.testutils.robolectric.RobolectricTest 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 import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) -class DefaultNotificationConversationServiceTest { +class DefaultNotificationConversationServiceTest : RobolectricTest() { @Test fun `onSendMessage adds a shortcut`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index c58a29d866..38b387a180 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -45,14 +45,12 @@ import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvid import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -@RunWith(RobolectricTestRunner::class) -class DefaultNotificationCreatorTest { +class DefaultNotificationCreatorTest : RobolectricTest() { @Test fun `test createDiagnosticNotification`() { val sut = createNotificationCreator() @@ -373,10 +371,12 @@ fun createNotificationChannels( enterpriseService: EnterpriseService = FakeEnterpriseService(), ): NotificationChannels { val context = RuntimeEnvironment.getApplication() + val notificationManagerCompat = NotificationManagerCompat.from(context) return DefaultNotificationChannels( - notificationManager = NotificationManagerCompat.from(context), + notificationManager = notificationManagerCompat, stringProvider = FakeStringProvider(""), context = context, enterpriseService = enterpriseService, + appPreferencesStore = io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore(), ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt index 33071ef1ac..a0c233bc39 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt @@ -57,7 +57,17 @@ class FakeNotificationCreator( events: List, ): Notification { return createMessagesListNotificationResult( - listOf(notificationAccountParams, roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, existingNotification, imageLoader, events) + listOf( + notificationAccountParams, + roomInfo, + threadId, + largeIcon, + lastMessageTimestamp, + tickerText, + existingNotification, + imageLoader, + events, + ) ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/sound/DefaultNotificationSoundCopierTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/sound/DefaultNotificationSoundCopierTest.kt new file mode 100644 index 0000000000..e57829b218 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/sound/DefaultNotificationSoundCopierTest.kt @@ -0,0 +1,80 @@ +/* + * 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.push.impl.notifications.sound + +import android.content.Context +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier +import io.element.android.tests.testutils.robolectric.RobolectricTest +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.robolectric.RuntimeEnvironment + +class DefaultNotificationSoundCopierTest : RobolectricTest() { + private val context: Context get() = RuntimeEnvironment.getApplication() + + @Test + fun `sanitizeDisplayName - trims surrounding whitespace`() { + assertThat(sanitizeDisplayName(" Hello World ")).isEqualTo("Hello World") + } + + @Test + fun `sanitizeDisplayName - DEL and control characters are dropped`() { + // Build the raw string programmatically — embedding control chars as source-file bytes + // doesn't survive editor / tool round-tripping reliably. + val bel = 0x07.toChar() + val tab = 0x09.toChar() + val newline = 0x0A.toChar() + val del = 0x7F.toChar() + val raw = "${bel}Hello$tab${newline}World$del" + assertThat(sanitizeDisplayName(raw)).isEqualTo("HelloWorld") + } + + @Test + fun `sanitizeDisplayName - truncates oversized input to 256 chars`() { + val long = "x".repeat(500) + val sanitized = sanitizeDisplayName(long) + assertThat(sanitized).isNotNull() + assertThat(sanitized!!.length).isEqualTo(256) + } + + @Test + fun `sanitizeDisplayName - returns null when nothing survives sanitization`() { + assertThat(sanitizeDisplayName("")).isNull() + assertThat(sanitizeDisplayName(" ")).isNull() + val controls = (0..0x1F).joinToString(separator = "") { it.toChar().toString() } + 0x7F.toChar() + assertThat(sanitizeDisplayName(controls)).isNull() + } + + @Test + fun `sanitizeDisplayName - preserves unicode beyond ASCII`() { + val input = "Element Tone " + 0x266A.toChar() + assertThat(sanitizeDisplayName(input)).isEqualTo(input) + } + + @Test + fun `copyToAppFiles - rejects file scheme`() = runTest { + val copier = DefaultNotificationSoundCopier(context) + val result = copier.copyToAppFiles("file:///tmp/evil.mp3", NotificationSoundCopier.SoundSlot.Message) + assertThat(result).isEqualTo(NotificationSoundCopier.CopyResult.UnplayableSource) + } + + @Test + fun `copyToAppFiles - rejects http scheme`() = runTest { + val copier = DefaultNotificationSoundCopier(context) + val result = copier.copyToAppFiles("https://attacker.example.com/sound.mp3", NotificationSoundCopier.SoundSlot.Call) + assertThat(result).isEqualTo(NotificationSoundCopier.CopyResult.UnplayableSource) + } + + @Test + fun `copyToAppFiles - rejects URI without a scheme`() = runTest { + val copier = DefaultNotificationSoundCopier(context) + val result = copier.copyToAppFiles("not-a-real-uri", NotificationSoundCopier.SoundSlot.Message) + assertThat(result).isEqualTo(NotificationSoundCopier.CopyResult.UnplayableSource) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManagerTest.kt index 63307634f3..2c0ca8e854 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultFetchPushForegroundServiceManagerTest.kt @@ -12,21 +12,19 @@ import android.content.ComponentName import android.content.Context.ACTIVITY_SERVICE import android.content.Context.POWER_SERVICE import android.os.PowerManager -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.async import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.Shadows import org.robolectric.shadows.ShadowActivityManager import org.robolectric.shadows.ShadowPowerManager import kotlin.time.Duration.Companion.seconds -@RunWith(AndroidJUnit4::class) -class DefaultFetchPushForegroundServiceManagerTest { +class DefaultFetchPushForegroundServiceManagerTest : RobolectricTest() { @Test fun `start should start the service if the device is not interactive`() { val manager = createDefaultFetchPushForegroundServiceManager() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt index 2bab4c14c2..8c7f9e0454 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt @@ -30,15 +30,13 @@ import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEv import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultOnRedactedEventReceivedTest { +class DefaultOnRedactedEventReceivedTest : RobolectricTest() { private val fakePerson = Person.Builder().setName(A_USER_NAME).setKey(A_USER_ID.value).build() private val fakeMessage = NotificationCompat.MessagingStyle.Message("A message", 0L, fakePerson).also { it.extras.putString(DefaultNotificationCreator.MESSAGE_EVENT_ID, AN_EVENT_ID.value) 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 a16568d400..f0dee4446c 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 @@ -11,6 +11,7 @@ package io.element.android.libraries.push.impl.push import app.cash.turbine.test +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -40,7 +41,9 @@ import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -212,7 +215,7 @@ class DefaultPushHandlerTest { .isCalledOnce() } - private fun createDefaultPushHandler( + private fun TestScope.createDefaultPushHandler( incrementPushCounterResult: () -> Unit = { lambdaError() }, userPushStore: FakeUserPushStore = FakeUserPushStore(), pushClientSecret: PushClientSecret = FakePushClientSecret(), @@ -227,6 +230,7 @@ class DefaultPushHandlerTest { start = {}, stop = {}, ), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), ): DefaultPushHandler { return DefaultPushHandler( incrementPushDataStore = object : IncrementPushDataStore { @@ -246,7 +250,8 @@ class DefaultPushHandlerTest { resultProcessor = resultProcessor, syncPendingNotificationsRequestFactory = SyncPendingNotificationsRequestBuilder.Factory { FakeSyncPendingNotificationsRequestBuilder() - } + }, + dispatchers = dispatchers, ) } } 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 e65bea20a9..49d0d12ec8 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 @@ -8,7 +8,6 @@ package io.element.android.libraries.push.impl.workmanager import android.net.NetworkCapabilities -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.work.OneTimeWorkRequest import androidx.work.hasKeyWithValueOfType import com.google.common.truth.Truth.assertThat @@ -21,14 +20,13 @@ import io.element.android.libraries.workmanager.api.WorkManagerRequestType import io.element.android.libraries.workmanager.api.WorkManagerWorkerType import io.element.android.libraries.workmanager.api.workManagerTag import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith import org.robolectric.annotation.Config @Config(sdk = [33]) -@RunWith(AndroidJUnit4::class) -class DefaultSyncPendingNotificationsRequestBuilderTest { +class DefaultSyncPendingNotificationsRequestBuilderTest : RobolectricTest() { @Test fun `build - success API 33`() = runTest { val request = createSyncPendingNotificationsRequestBuilder( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt index c0fa5d3442..889fb878d2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchPendingNotificationWorkerTest.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.push.impl.workmanager -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.work.Data import androidx.work.ListenableWorker @@ -34,12 +33,12 @@ import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith import java.util.UUID import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -48,8 +47,7 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.Instant @OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) -class FetchPendingNotificationWorkerTest { +class FetchPendingNotificationWorkerTest : RobolectricTest() { @Test fun `test - success`() = runTest { var synced = false diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 9dedeb3996..d125a2387e 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.workmanager.api) implementation(projects.tests.testutils) diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationSoundUpdater.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationSoundUpdater.kt new file mode 100644 index 0000000000..1bbd410aec --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeNotificationSoundUpdater.kt @@ -0,0 +1,31 @@ +/* + * 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.push.test.notifications + +import io.element.android.libraries.preferences.api.store.NotificationSound +import io.element.android.libraries.push.api.notifications.NotificationSoundUpdater +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeNotificationSoundUpdater( + private val recreateNoisyChannelLambda: (sound: NotificationSound, version: Int) -> Unit = { _, _ -> lambdaError() }, + private val recreateRingingCallChannelLambda: (sound: NotificationSound, version: Int) -> Unit = { _, _ -> lambdaError() }, + private val readNoisyChannelSoundLambda: () -> NotificationSound? = { null }, + private val readRingingCallChannelSoundLambda: () -> NotificationSound? = { null }, +) : NotificationSoundUpdater { + override fun recreateNoisyChannel(sound: NotificationSound, version: Int) { + recreateNoisyChannelLambda(sound, version) + } + + override fun recreateRingingCallChannel(sound: NotificationSound, version: Int) { + recreateRingingCallChannelLambda(sound, version) + } + + override suspend fun readNoisyChannelSound(): NotificationSound? = readNoisyChannelSoundLambda() + + override suspend fun readRingingCallChannelSound(): NotificationSound? = readRingingCallChannelSoundLambda() +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeSoundDisplayNameResolver.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeSoundDisplayNameResolver.kt new file mode 100644 index 0000000000..69bb77b65c --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeSoundDisplayNameResolver.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.push.test.notifications + +import io.element.android.libraries.push.api.notifications.SoundDisplayNameResolver + +class FakeSoundDisplayNameResolver( + // Default resolves to a non-null sentinel so a default-constructed fake doesn't masquerade + // as "every Custom URI is unresolvable" — that would trip mid-session detection paths in + // every test that uses default fakes. Tests that need the unavailable case pass `{ null }`. + private val resolveLambda: suspend (uri: String) -> String? = { "FakeRingtoneTitle" }, +) : SoundDisplayNameResolver { + override suspend fun resolveCustomSoundTitle(uri: String): String? = resolveLambda(uri) +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/conversations/FakeNotificationConversationService.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/conversations/FakeNotificationConversationService.kt index 0c8d870448..a2022ea22d 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/conversations/FakeNotificationConversationService.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/conversations/FakeNotificationConversationService.kt @@ -16,7 +16,7 @@ class FakeNotificationConversationService : NotificationConversationService { override suspend fun onSendMessage( sessionId: SessionId, roomId: RoomId, - roomName: String, + roomName: String?, roomIsDirect: Boolean, roomAvatarUrl: String?, ) = Unit diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/sound/FakeNotificationSoundCopier.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/sound/FakeNotificationSoundCopier.kt new file mode 100644 index 0000000000..9fe88b7718 --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/sound/FakeNotificationSoundCopier.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.libraries.push.test.notifications.sound + +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier.CopyResult +import io.element.android.libraries.push.api.notifications.sound.NotificationSoundCopier.SoundSlot +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeNotificationSoundCopier( + private val copyLambda: (String, SoundSlot) -> CopyResult = { _, _ -> lambdaError() }, + private val deleteStoredSoundForLambda: (SoundSlot) -> Unit = {}, +) : NotificationSoundCopier { + override suspend fun copyToAppFiles(sourceUriString: String, slot: SoundSlot): CopyResult = copyLambda(sourceUriString, slot) + + override suspend fun deleteStoredSoundFor(slot: SoundSlot) { + deleteStoredSoundForLambda(slot) + } +} diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 49ce7135d5..1a8e73a4da 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -11,6 +11,8 @@ import config.BuildTimeConfig import extension.setupDependencyInjection import extension.testCommonDependencies +import org.gradle.kotlin.dsl.withType +import org.sonarqube.gradle.SonarResolverTask plugins { id("io.element.android-library") @@ -19,6 +21,10 @@ plugins { android { namespace = "io.element.android.libraries.pushproviders.firebase" + buildFeatures { + resValues = true + } + buildTypes { getByName("release") { consumerProguardFiles("consumer-proguard-rules.pro") @@ -47,6 +53,11 @@ android { } } +// Configure the SonarQube plugin to wait for the resource generation tasks to complete before running the analysis. +tasks.withType().configureEach { + dependsOn("generateDebugResValues", "generateDebugAndroidTestResValues") +} + setupDependencyInjection() dependencies { @@ -57,6 +68,7 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.troubleshoot.api) implementation(projects.services.toolbox.api) diff --git a/libraries/pushproviders/firebase/src/main/AndroidManifest.xml b/libraries/pushproviders/firebase/src/main/AndroidManifest.xml index 24bad30d77..9a6972e412 100644 --- a/libraries/pushproviders/firebase/src/main/AndroidManifest.xml +++ b/libraries/pushproviders/firebase/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ --> + + { - val pushKey = firebaseStore.getFcmToken() ?: return Result.failure( + val pushKey = firebaseStore.getInstallationId() ?: return Result.failure( IllegalStateException( "Unable to register pusher, Firebase token is not known." ) @@ -59,7 +59,7 @@ class FirebasePushProvider( override suspend fun getCurrentDistributor(sessionId: SessionId) = firebaseDistributor override suspend fun unregister(matrixClient: MatrixClient): Result { - val pushKey = firebaseStore.getFcmToken() + val pushKey = firebaseStore.getInstallationId() return if (pushKey == null) { Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.") Result.success(Unit) @@ -74,7 +74,7 @@ class FirebasePushProvider( override suspend fun onSessionDeleted(sessionId: SessionId) = Unit override suspend fun getPushConfig(sessionId: SessionId): Config? { - return firebaseStore.getFcmToken()?.let { fcmToken -> + return firebaseStore.getInstallationId()?.let { fcmToken -> Config( url = firebaseGatewayProvider.getFirebaseGateway(), pushKey = fcmToken @@ -85,7 +85,7 @@ class FirebasePushProvider( override fun canRotateToken(): Boolean = true override suspend fun rotateToken(): Result { - return firebaseTokenRotator.rotate() + return rotateFirebaseSession() } companion object { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt index 1f6a4709cc..748d540a54 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt @@ -18,28 +18,28 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart /** - * This class store the Firebase token in SharedPrefs. + * This class stores the Firebase installationId in SharedPrefs. */ interface FirebaseStore { - fun getFcmToken(): String? - fun fcmTokenFlow(): Flow - fun storeFcmToken(token: String?) + fun getInstallationId(): String? + fun fcmInstallationIdFlow(): Flow + fun storeInstallationId(installationId: String?) } @ContributesBinding(AppScope::class) class SharedPreferencesFirebaseStore( private val sharedPreferences: SharedPreferences, ) : FirebaseStore { - override fun getFcmToken(): String? { - return sharedPreferences.getString(PREFS_KEY_FCM_TOKEN, null) + override fun getInstallationId(): String? { + return sharedPreferences.getString(PREFS_KEY_FCM_INSTALLATION_ID, null) } - override fun fcmTokenFlow(): Flow { - val flow = MutableStateFlow(getFcmToken()) + override fun fcmInstallationIdFlow(): Flow { + val flow = MutableStateFlow(getInstallationId()) val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, k -> - if (k == PREFS_KEY_FCM_TOKEN) { + if (k == PREFS_KEY_FCM_INSTALLATION_ID) { try { - flow.value = getFcmToken() + flow.value = getInstallationId() } catch (_: Exception) { flow.value = null } @@ -50,13 +50,13 @@ class SharedPreferencesFirebaseStore( .onCompletion { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } } - override fun storeFcmToken(token: String?) { + override fun storeInstallationId(installationId: String?) { sharedPreferences.edit { - putString(PREFS_KEY_FCM_TOKEN, token) + putString(PREFS_KEY_FCM_INSTALLATION_ID, installationId) } } companion object { - private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" + private const val PREFS_KEY_FCM_INSTALLATION_ID = "FCM_TOKEN" } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt deleted file mode 100644 index 21fa189121..0000000000 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenDeleter.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.pushproviders.firebase - -import com.google.firebase.messaging.FirebaseMessaging -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -interface FirebaseTokenDeleter { - /** - * Deletes the current Firebase token. - */ - suspend fun delete() -} - -@ContributesBinding(AppScope::class) -class DefaultFirebaseTokenDeleter( - private val isPlayServiceAvailable: IsPlayServiceAvailable, -) : FirebaseTokenDeleter { - override suspend fun delete() { - // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' - isPlayServiceAvailable.checkAvailableOrThrow() - suspendCoroutine { continuation -> - try { - FirebaseMessaging.getInstance().deleteToken() - .addOnSuccessListener { - continuation.resume(Unit) - } - .addOnFailureListener { e -> - Timber.e(e, "## deleteFirebaseToken() : failed") - continuation.resumeWithException(e) - } - } catch (e: Throwable) { - Timber.e(e, "## deleteFirebaseToken() : failed") - continuation.resumeWithException(e) - } - } - } -} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt deleted file mode 100644 index 80fd9726d4..0000000000 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenGetter.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.pushproviders.firebase - -import com.google.firebase.messaging.FirebaseMessaging -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -interface FirebaseTokenGetter { - /** - * Read the current Firebase token from FirebaseMessaging. - * If the token does not exist, it will be generated. - */ - suspend fun get(): String -} - -@ContributesBinding(AppScope::class) -class DefaultFirebaseTokenGetter( - private val isPlayServiceAvailable: IsPlayServiceAvailable, -) : FirebaseTokenGetter { - override suspend fun get(): String { - // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' - isPlayServiceAvailable.checkAvailableOrThrow() - return suspendCoroutine { continuation -> - try { - FirebaseMessaging.getInstance().token - .addOnSuccessListener { token -> - continuation.resume(token) - } - .addOnFailureListener { e -> - Timber.e(e, "## retrievedFirebaseToken() : failed") - continuation.resumeWithException(e) - } - } catch (e: Throwable) { - Timber.e(e, "## retrievedFirebaseToken() : failed") - continuation.resumeWithException(e) - } - } - } -} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt deleted file mode 100644 index 78b33cbb08..0000000000 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.pushproviders.firebase - -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.core.extensions.runCatchingExceptions - -interface FirebaseTokenRotator { - suspend fun rotate(): Result -} - -/** - * This class delete the Firebase token and generate a new one. - */ -@ContributesBinding(AppScope::class) -class DefaultFirebaseTokenRotator( - private val firebaseTokenDeleter: FirebaseTokenDeleter, - private val firebaseTokenGetter: FirebaseTokenGetter, -) : FirebaseTokenRotator { - override suspend fun rotate(): Result { - return runCatchingExceptions { - firebaseTokenDeleter.delete() - firebaseTokenGetter.get() - } - } -} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt index 78fce0338d..0e58133a60 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.pushproviders.firebase import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.core.extensions.runCatchingExceptions interface FirebaseTroubleshooter { suspend fun troubleshoot(): Result @@ -21,13 +20,9 @@ interface FirebaseTroubleshooter { */ @ContributesBinding(AppScope::class) class DefaultFirebaseTroubleshooter( - private val newTokenHandler: FirebaseNewTokenHandler, - private val firebaseTokenGetter: FirebaseTokenGetter, + private val rotateFirebaseSession: RotateFirebaseSession, ) : FirebaseTroubleshooter { override suspend fun troubleshoot(): Result { - return runCatchingExceptions { - val token = firebaseTokenGetter.get() - newTokenHandler.handle(token) - } + return rotateFirebaseSession() } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/RegisterFirebaseSession.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/RegisterFirebaseSession.kt new file mode 100644 index 0000000000..42604bd013 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/RegisterFirebaseSession.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2023-2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.pushproviders.firebase + +import com.google.firebase.messaging.FirebaseMessaging +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.pushproviders.firebase.util.runFirebaseTask +import timber.log.Timber + +fun interface RegisterFirebaseSession { + /** + * Register the device to Firebase Messaging. + */ + suspend operator fun invoke(): Result +} + +@ContributesBinding(AppScope::class) +class DefaultRegisterFirebaseSession( + private val isPlayServiceAvailable: IsPlayServiceAvailable, +) : RegisterFirebaseSession { + override suspend operator fun invoke(): Result { + // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' + isPlayServiceAvailable.checkAvailableOrThrow() + return runFirebaseTask { FirebaseMessaging.getInstance().register() } + .onFailure { Timber.e(it, "## registerFirebaseMessaging() : failed") } + // Change return type from Void! to Unit + .map {} + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/RotateFirebaseSession.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/RotateFirebaseSession.kt new file mode 100644 index 0000000000..a6e51dafa2 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/RotateFirebaseSession.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2024, 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.pushproviders.firebase + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions + +fun interface RotateFirebaseSession { + suspend operator fun invoke(): Result +} + +/** + * This class deletes the Firebase installation id and generates a new one. + */ +@ContributesBinding(AppScope::class) +class DefaultRotateFirebaseSession( + private val registerFirebaseSession: RegisterFirebaseSession, + private val unregisterFirebaseSession: UnregisterFirebaseSession, +) : RotateFirebaseSession { + override suspend operator fun invoke(): Result { + return runCatchingExceptions { + // Stop the current session, which will also delete the existing installation id from Firebase + unregisterFirebaseSession().getOrThrow() + // Register again to get a new installation id + registerFirebaseSession().getOrThrow() + } + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/UnregisterFirebaseSession.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/UnregisterFirebaseSession.kt new file mode 100644 index 0000000000..815f2d734c --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/UnregisterFirebaseSession.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2023-2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.pushproviders.firebase + +import com.google.firebase.installations.FirebaseInstallations +import com.google.firebase.messaging.FirebaseMessaging +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.pushproviders.firebase.util.runFirebaseTask +import timber.log.Timber + +fun interface UnregisterFirebaseSession { + /** + * Deletes the current Firebase token. + */ + suspend operator fun invoke(): Result +} + +@ContributesBinding(AppScope::class) +class DefaultUnregisterFirebaseSession( + private val isPlayServiceAvailable: IsPlayServiceAvailable, +) : UnregisterFirebaseSession { + override suspend operator fun invoke(): Result { + // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' + isPlayServiceAvailable.checkAvailableOrThrow() + return runFirebaseTask { + // Unregister the device from Firebase Messaging + FirebaseMessaging.getInstance().unregister() + // Also delete the existing installation id from Firebase + .continueWithTask { FirebaseInstallations.getInstance().delete() } + } + .onFailure { + Timber.e(it, "## unregisterFirebaseMessaging() : failed") + } + // Change return type from Void! to Unit + .map {} + } +} 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 975a3c75ca..cd6ad3c8bc 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 @@ -24,7 +24,7 @@ import timber.log.Timber private val loggerTag = LoggerTag("VectorFirebaseMessagingService", LoggerTag.PushLoggerTag) class VectorFirebaseMessagingService : FirebaseMessagingService() { - @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler + @Inject lateinit var firebaseNewInstallationIdHandler: FirebaseNewInstallationIdHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler @Inject lateinit var fetchPushForegroundServiceManager: FetchPushForegroundServiceManager @@ -36,10 +36,12 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { bindings().inject(this) } - override fun onNewToken(token: String) { - Timber.tag(loggerTag.value).w("New Firebase token") + override fun onRegistered(installationId: String) { + super.onRegistered(installationId) + + Timber.tag(loggerTag.value).w("New Firebase installation id") coroutineScope.launch { - firebaseNewTokenHandler.handle(token) + firebaseNewInstallationIdHandler.handle(installationId) } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt index 7ea7c6cee2..c3af8df542 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt @@ -49,7 +49,7 @@ class FirebaseTokenTest( override suspend fun run(coroutineScope: CoroutineScope) { currentJob?.cancel() delegate.start() - currentJob = firebaseStore.fcmTokenFlow() + currentJob = firebaseStore.fcmInstallationIdFlow() .onEach { token -> if (token != null) { delegate.updateState( diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/util/RunFirebaseTask.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/util/RunFirebaseTask.kt new file mode 100644 index 0000000000..0ddde69d1e --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/util/RunFirebaseTask.kt @@ -0,0 +1,35 @@ +/* + * 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.pushproviders.firebase.util + +import com.google.android.gms.tasks.Task +import io.element.android.libraries.core.extensions.runCatchingExceptions +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +suspend fun runFirebaseTaskWithResult(closure: () -> Task): Result { + return runCatchingExceptions { + suspendCancellableCoroutine { continuation -> + try { + closure().addOnSuccessListener { + continuation.resume(it) + }.addOnFailureListener { e -> + continuation.resumeWithException(e) + } + } catch (e: Throwable) { + continuation.resumeWithException(e) + } + } + } +} + +// Special case for `Void!` return type, which is not a valid Kotlin type. We convert it to `Unit` instead. +suspend fun runFirebaseTask(closure: () -> Task): Result { + return runFirebaseTaskWithResult { closure().continueWith {} } +} diff --git a/libraries/pushproviders/firebase/src/main/res/values-ca/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..acd61bab06 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ca/translations.xml @@ -0,0 +1,11 @@ + + + "Assegura\'t que Firebase estigui disponible." + "Firebase no està disponible." + "Firebase està disponible." + "Comprova Firebase" + "Assegura\'t que el token de Firebase està disponible." + "Token de Firebase desconegut." + "Token de Firebase: %1$s." + "Comprova el token de Firebase" + diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewInstallationIdHandlerTest.kt similarity index 96% rename from libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt rename to libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewInstallationIdHandlerTest.kt index 4db9ea667f..78d35fe2a5 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewInstallationIdHandlerTest.kt @@ -30,16 +30,16 @@ import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.runTest import org.junit.Test -class DefaultFirebaseNewTokenHandlerTest { +class DefaultFirebaseNewInstallationIdHandlerTest { @Test fun `when a new token is received it is stored in the firebase store`() = runTest { val firebaseStore = InMemoryFirebaseStore() - assertThat(firebaseStore.getFcmToken()).isNull() + assertThat(firebaseStore.getInstallationId()).isNull() val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( firebaseStore = firebaseStore, ) firebaseNewTokenHandler.handle("aToken") - assertThat(firebaseStore.getFcmToken()).isEqualTo("aToken") + assertThat(firebaseStore.getInstallationId()).isEqualTo("aToken") } @Test @@ -142,8 +142,8 @@ class DefaultFirebaseNewTokenHandlerTest { matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), firebaseStore: FirebaseStore = InMemoryFirebaseStore(), firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider(), - ): FirebaseNewTokenHandler { - return DefaultFirebaseNewTokenHandler( + ): FirebaseNewInstallationIdHandler { + return DefaultFirebaseNewInstallationIdHandler( pusherSubscriber = pusherSubscriber, sessionStore = sessionStore, userPushStoreFactory = userPushStoreFactory, diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewInstallationIdHandler.kt similarity index 71% rename from libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewTokenHandler.kt rename to libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewInstallationIdHandler.kt index 2fa9508103..b6b310fbb8 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewInstallationIdHandler.kt @@ -10,10 +10,10 @@ package io.element.android.libraries.pushproviders.firebase import io.element.android.tests.testutils.lambda.lambdaError -class FakeFirebaseNewTokenHandler( +class FakeFirebaseNewInstallationIdHandler( private val handleResult: (String) -> Unit = { lambdaError() } -) : FirebaseNewTokenHandler { - override suspend fun handle(firebaseToken: String) { - handleResult(firebaseToken) +) : FirebaseNewInstallationIdHandler { + override suspend fun handle(installationId: String) { + handleResult(installationId) } } diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTokenRotator.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeRotateFirebaseSession.kt similarity index 81% rename from libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTokenRotator.kt rename to libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeRotateFirebaseSession.kt index d504aa32ae..34ae929af8 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTokenRotator.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeRotateFirebaseSession.kt @@ -10,10 +10,10 @@ package io.element.android.libraries.pushproviders.firebase import io.element.android.tests.testutils.lambda.lambdaError -class FakeFirebaseTokenRotator( +class FakeRotateFirebaseSession( private val rotateWithResult: () -> Result = { lambdaError() } -) : FirebaseTokenRotator { - override suspend fun rotate(): Result { +) : RotateFirebaseSession { + override suspend fun invoke(): Result { return rotateWithResult() } } diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt index 312689da6e..a2737bc377 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt @@ -61,7 +61,7 @@ class FirebasePushProviderTest { val registerPusherResultLambda = lambdaRecorder> { _, _, _ -> Result.success(Unit) } val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = "aToken" + installationId = "aToken" ), pusherSubscriber = FakePusherSubscriber( registerPusherResult = registerPusherResultLambda @@ -78,7 +78,7 @@ class FirebasePushProviderTest { fun `register ko no token`() = runTest { val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = null + installationId = null ), pusherSubscriber = FakePusherSubscriber( registerPusherResult = { _, _, _ -> Result.success(Unit) } @@ -92,7 +92,7 @@ class FirebasePushProviderTest { fun `register ko error`() = runTest { val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = "aToken" + installationId = "aToken" ), pusherSubscriber = FakePusherSubscriber( registerPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) } @@ -108,7 +108,7 @@ class FirebasePushProviderTest { val unregisterPusherResultLambda = lambdaRecorder> { _, _, _ -> Result.success(Unit) } val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = "aToken" + installationId = "aToken" ), pusherSubscriber = FakePusherSubscriber( unregisterPusherResult = unregisterPusherResultLambda @@ -125,7 +125,7 @@ class FirebasePushProviderTest { fun `unregister no token - in this case, the error is ignored`() = runTest { val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = null + installationId = null ), ) val result = firebasePushProvider.unregister(FakeMatrixClient()) @@ -136,7 +136,7 @@ class FirebasePushProviderTest { fun `unregister ko error`() = runTest { val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = "aToken" + installationId = "aToken" ), pusherSubscriber = FakePusherSubscriber( unregisterPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) } @@ -150,7 +150,7 @@ class FirebasePushProviderTest { fun `getCurrentUserPushConfig no push ket`() = runTest { val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = null + installationId = null ) ) val result = firebasePushProvider.getPushConfig(A_SESSION_ID) @@ -161,7 +161,7 @@ class FirebasePushProviderTest { fun `getCurrentUserPushConfig ok`() = runTest { val firebasePushProvider = createFirebasePushProvider( firebaseStore = InMemoryFirebaseStore( - token = "aToken" + installationId = "aToken" ), ) val result = firebasePushProvider.getPushConfig(A_SESSION_ID) @@ -169,10 +169,10 @@ class FirebasePushProviderTest { } @Test - fun `rotateToken invokes the FirebaseTokenRotator`() = runTest { + fun `rotateToken invokes the FirebaseMessagingSessionRotator`() = runTest { val lambda = lambdaRecorder> { Result.success(Unit) } val firebasePushProvider = createFirebasePushProvider( - firebaseTokenRotator = FakeFirebaseTokenRotator(lambda), + rotateFirebaseSession = FakeRotateFirebaseSession(lambda), ) firebasePushProvider.rotateToken() lambda.assertions().isCalledOnce() @@ -194,14 +194,14 @@ class FirebasePushProviderTest { firebaseStore: FirebaseStore = InMemoryFirebaseStore(), pusherSubscriber: PusherSubscriber = FakePusherSubscriber(), isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false), - firebaseTokenRotator: FirebaseTokenRotator = FakeFirebaseTokenRotator(), + rotateFirebaseSession: RotateFirebaseSession = FakeRotateFirebaseSession(), firebaseGatewayProvider: FirebaseGatewayProvider = FakeFirebaseGatewayProvider() ): FirebasePushProvider { return FirebasePushProvider( firebaseStore = firebaseStore, pusherSubscriber = pusherSubscriber, isPlayServiceAvailable = isPlayServiceAvailable, - firebaseTokenRotator = firebaseTokenRotator, + rotateFirebaseSession = rotateFirebaseSession, firebaseGatewayProvider = firebaseGatewayProvider, ) } diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt index 3e6292f445..9e55cdcb4d 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf class InMemoryFirebaseStore( - private var token: String? = null + private var installationId: String? = null ) : FirebaseStore { - override fun getFcmToken(): String? = token + override fun getInstallationId(): String? = installationId - override fun fcmTokenFlow(): Flow = flowOf(token) + override fun fcmInstallationIdFlow(): Flow = flowOf(installationId) - override fun storeFcmToken(token: String?) { - this.token = token + override fun storeInstallationId(installationId: String?) { + this.installationId = installationId } } 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 a04c961ebb..d640e3c858 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 @@ -21,17 +21,15 @@ import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle 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 VectorFirebaseMessagingServiceTest { +class VectorFirebaseMessagingServiceTest : RobolectricTest() { @Test fun `test receiving invalid data`() = runTest { val lambda = lambdaRecorder { _, _ -> } @@ -173,22 +171,22 @@ class VectorFirebaseMessagingServiceTest { fun `test new token is forwarded to the handler`() = runTest { val lambda = lambdaRecorder { } val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( - firebaseNewTokenHandler = FakeFirebaseNewTokenHandler(handleResult = lambda) + firebaseNewInstallationIdHandler = FakeFirebaseNewInstallationIdHandler(handleResult = lambda) ) - vectorFirebaseMessagingService.onNewToken("aToken") + vectorFirebaseMessagingService.onRegistered("installationId") advanceUntilIdle() lambda.assertions() .isCalledOnce() - .with(value("aToken")) + .with(value("installationId")) } private fun TestScope.createVectorFirebaseMessagingService( - firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(), + firebaseNewInstallationIdHandler: FirebaseNewInstallationIdHandler = FakeFirebaseNewInstallationIdHandler(), pushHandler: PushHandler = FakePushHandler(), pushHandlingWakeLock: FakeFetchPushForegroundServiceManager = FakeFetchPushForegroundServiceManager(), ): VectorFirebaseMessagingService { return VectorFirebaseMessagingService().apply { - this.firebaseNewTokenHandler = firebaseNewTokenHandler + this.firebaseNewInstallationIdHandler = firebaseNewInstallationIdHandler this.pushParser = FirebasePushParser() this.pushHandler = pushHandler this.coroutineScope = this@createVectorFirebaseMessagingService diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt index 8f9721a17d..963a4352ec 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt @@ -45,7 +45,7 @@ class FirebaseTokenTestTest { firebaseStore = firebaseStore, firebaseTroubleshooter = FakeFirebaseTroubleshooter( troubleShootResult = { - firebaseStore.storeFcmToken(FAKE_TOKEN) + firebaseStore.storeInstallationId(FAKE_TOKEN) Result.success(Unit) } ), @@ -70,7 +70,7 @@ class FirebaseTokenTestTest { firebaseStore = firebaseStore, firebaseTroubleshooter = FakeFirebaseTroubleshooter( troubleShootResult = { - firebaseStore.storeFcmToken(FAKE_TOKEN) + firebaseStore.storeInstallationId(FAKE_TOKEN) Result.success(Unit) } ), diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt deleted file mode 100644 index 4a8eefe37f..0000000000 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/GuardServiceStarter.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.pushproviders.unifiedpush - -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.ContributesBinding - -interface GuardServiceStarter { - fun start() {} - fun stop() {} -} - -@ContributesBinding(AppScope::class) -class NoopGuardServiceStarter : GuardServiceStarter diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt index 78a569a211..1155293567 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayUrlResolver.kt @@ -29,8 +29,9 @@ class DefaultUnifiedPushGatewayUrlResolver( ): String { return when (gatewayResult) { is UnifiedPushGatewayResolverResult.Error -> { - // Use previous gateway if any, or the provided one - unifiedPushStore.getPushGateway(instance) ?: gatewayResult.gateway + // Use previous gateway if any, or the default one + unifiedPushStore.getPushGateway(instance) + ?: defaultPushGatewayHttpUrlProvider.provide() } UnifiedPushGatewayResolverResult.ErrorInvalidUrl, UnifiedPushGatewayResolverResult.NoMatrixGateway -> { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 4288f2b6b8..b1e9ba648b 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -31,7 +31,6 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver", LoggerTa class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushParser: UnifiedPushParser @Inject lateinit var pushHandler: PushHandler - @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushStore: UnifiedPushStore @Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver @Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver @@ -112,7 +111,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { ) ) } - guardServiceStarter.stop() } /** diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ca/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..51096a1cfa --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ca/translations.xml @@ -0,0 +1,10 @@ + + + "Assegura\'t que hi hagi distribuïdors d\'UnifiedPush disponibles." + "No s\'han trobat distribuïdors push." + + "%1$d distribuïdor trobat: %2$s." + "%1$d distribuïdors trobats: %2$s." + + "Prova UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt index 1e77df79a2..fe7da75951 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt @@ -16,17 +16,15 @@ import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult import io.element.android.tests.testutils.fake.FakeAndroidKeyStore +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultRegisterUnifiedPushUseCaseTest { +class DefaultRegisterUnifiedPushUseCaseTest : RobolectricTest() { @Before fun setup() { FakeAndroidKeyStore.setup diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt index 3f437bdafa..664ff26d15 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayUrlResolverTest.kt @@ -60,7 +60,7 @@ class DefaultUnifiedPushGatewayUrlResolverTest { } @Test - fun `resolve Error returns the url if no current url is available`() { + fun `resolve Error returns the default gateway url if no current url is available`() { val sut = createDefaultUnifiedPushGatewayUrlResolver( unifiedPushStore = FakeUnifiedPushStore( getPushGatewayResult = { instance -> @@ -73,7 +73,7 @@ class DefaultUnifiedPushGatewayUrlResolverTest { gatewayResult = UnifiedPushGatewayResolverResult.Error("aUrl"), instance = "instance", ) - assertThat(result).isEqualTo("aUrl") + assertThat(result).isEqualTo(FakeDefaultPushGatewayHttpUrlProvider().provide()) } private fun createDefaultUnifiedPushGatewayUrlResolver( diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt index ed2337c5bb..e6454406d5 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt @@ -18,13 +18,11 @@ import io.element.android.libraries.push.test.FakePusherSubscriber import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class DefaultUnregisterUnifiedPushUseCaseTest { +class DefaultUnregisterUnifiedPushUseCaseTest : RobolectricTest() { @Test fun `test un registration successful`() = runTest { val lambda = lambdaRecorder { _: MatrixClient, _: String, _: String -> Result.success(Unit) } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt index cb23289c85..6d86cb2b9d 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt @@ -27,21 +27,19 @@ import io.element.android.libraries.pushproviders.unifiedpush.registration.Regis import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner import org.unifiedpush.android.connector.FailedReason import org.unifiedpush.android.connector.data.PublicKeySet import org.unifiedpush.android.connector.data.PushEndpoint import org.unifiedpush.android.connector.data.PushMessage -@RunWith(RobolectricTestRunner::class) -class VectorUnifiedPushMessagingReceiverTest { +class VectorUnifiedPushMessagingReceiverTest : RobolectricTest() { @Test fun `onReceive does the binding`() = runTest { val context = InstrumentationRegistry.getInstrumentation().context @@ -268,7 +266,6 @@ class VectorUnifiedPushMessagingReceiverTest { return VectorUnifiedPushMessagingReceiver().apply { this.pushParser = unifiedPushParser this.pushHandler = pushHandler - this.guardServiceStarter = NoopGuardServiceStarter() this.unifiedPushStore = unifiedPushStore this.unifiedPushGatewayResolver = unifiedPushGatewayResolver this.unifiedPushGatewayUrlResolver = unifiedPushGatewayUrlResolver diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 8ad2d62b33..fe9494a2c8 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -56,7 +56,6 @@ class UserPushStoreDataStore( private val currentPushKey = stringPreferencesKey("currentPushKey") private val notificationEnabled = booleanPreferencesKey("notificationEnabled") private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError") - override suspend fun getPushProviderName(): String? { return store.data.first()[pushProviderName] } diff --git a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt index 9191ea4ab4..773a5e3430 100644 --- a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt @@ -14,14 +14,12 @@ 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.A_SESSION_ID_2 import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class UserPushStoreDataStoreTest { +class UserPushStoreDataStoreTest : RobolectricTest() { @Test fun `test getPushProviderName`() = runTest { val sut = createUserPushStoreDataStore() diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt index 18bd1cff10..cae7452cd8 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt @@ -18,6 +18,7 @@ import androidx.camera.view.PreviewView import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -114,7 +115,8 @@ fun QrCodeCameraView( Box(modifier.clipToBounds()) { if (LocalInspectionMode.current) { Box( - modifier = modifier + modifier = Modifier + .fillMaxSize() .background(color = ElementTheme.colors.bgSubtlePrimary), contentAlignment = Alignment.Center, ) { diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeImage.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeImage.kt index e045e42f17..b50b202e2c 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeImage.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeImage.kt @@ -57,8 +57,8 @@ private fun BitMatrix.toBitmap( @Composable fun QrCodeImage( data: String, - forceMaxBrightness: Boolean = true, modifier: Modifier = Modifier, + forceMaxBrightness: Boolean = true, ) { if (forceMaxBrightness) { ForceMaxBrightness() diff --git a/libraries/recentemojis/impl/build.gradle.kts b/libraries/recentemojis/impl/build.gradle.kts index a1a72c8672..061a7ecd89 100644 --- a/libraries/recentemojis/impl/build.gradle.kts +++ b/libraries/recentemojis/impl/build.gradle.kts @@ -21,6 +21,7 @@ setupDependencyInjection() dependencies { api(projects.libraries.recentemojis.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(libs.kotlinx.collections.immutable) implementation(libs.matrix.emojibase.bindings) diff --git a/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt b/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt index ba5ca79d40..3c5c5d5ac3 100644 --- a/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt +++ b/libraries/roomselect/api/src/main/kotlin/io/element/android/libraries/roomselect/api/RoomSelectEntryPoint.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.core.RoomId interface RoomSelectEntryPoint : FeatureEntryPoint { data class Params( val mode: RoomSelectMode, + val maxNumberOfRooms: Int, ) fun createNode( @@ -30,4 +31,8 @@ interface RoomSelectEntryPoint : FeatureEntryPoint { fun onRoomSelected(roomIds: List) fun onCancel() } + + companion object { + const val DEFAULT_MAX_NUMBER_OF_ROOMS = 10 + } } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt index 4ca92663a9..ff3ba06f0d 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPoint.kt @@ -26,7 +26,10 @@ class DefaultRoomSelectEntryPoint : RoomSelectEntryPoint { return parentNode.createNode( buildContext = buildContext, plugins = listOf( - RoomSelectNode.Inputs(mode = params.mode), + RoomSelectNode.Inputs( + mode = params.mode, + maxNumberOfRooms = params.maxNumberOfRooms, + ), callback, ) ) diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt similarity index 60% rename from libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt rename to libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt index a3512c419c..13c5bc5f8d 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvents.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectEvent.kt @@ -10,11 +10,8 @@ package io.element.android.libraries.roomselect.impl import io.element.android.libraries.matrix.ui.model.SelectRoomInfo -sealed interface RoomSelectEvents { - data class SetSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvents - - // TODO remove to restore multi-selection - data object RemoveSelectedRoom : RoomSelectEvents - data object ToggleSearchActive : RoomSelectEvents - data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvents +sealed interface RoomSelectEvent { + data class ToggleSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvent + data object ToggleSearchActive : RoomSelectEvent + data class UpdateVisibleRange(val range: IntRange) : RoomSelectEvent } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt index 943da643c6..0297f13066 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectNode.kt @@ -35,10 +35,14 @@ class RoomSelectNode( ) : Node(buildContext, plugins = plugins) { data class Inputs( val mode: RoomSelectMode, + val maxNumberOfRooms: Int, ) : NodeInputs private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.mode) + private val presenter = presenterFactory.create( + mode = inputs.mode, + maxNumberOfRooms = inputs.maxNumberOfRooms, + ) private val stateFlow = launchMolecule { presenter.present() } private val callback: RoomSelectEntryPoint.Callback = callback() diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index b7547b8a45..85ecfe5ba7 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -34,11 +34,15 @@ import kotlinx.coroutines.launch @AssistedInject class RoomSelectPresenter( @Assisted private val mode: RoomSelectMode, + @Assisted private val maxNumberOfRooms: Int, private val dataSourceFactory: RoomSelectSearchDataSource.Factory, ) : Presenter { @AssistedFactory fun interface Factory { - fun create(mode: RoomSelectMode): RoomSelectPresenter + fun create( + mode: RoomSelectMode, + maxNumberOfRooms: Int, + ): RoomSelectPresenter } @Composable @@ -67,21 +71,18 @@ class RoomSelectPresenter( } } - fun handleEvent(event: RoomSelectEvents) { + fun handleEvent(event: RoomSelectEvent) { when (event) { - is RoomSelectEvents.SetSelectedRoom -> { - selectedRooms = persistentListOf(event.room) - // Restore for multi-selection -// val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId } -// selectedRooms = if (index >= 0) { -// selectedRooms.removeAt(index) -// } else { -// selectedRooms.add(event.room) -// } + is RoomSelectEvent.ToggleSelectedRoom -> { + val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId } + selectedRooms = if (index >= 0) { + selectedRooms.removingAt(index) + } else { + selectedRooms.adding(event.room) + } } - RoomSelectEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf() - RoomSelectEvents.ToggleSearchActive -> isSearchActive = !isSearchActive - is RoomSelectEvents.UpdateVisibleRange -> coroutineScope.launch { + RoomSelectEvent.ToggleSearchActive -> isSearchActive = !isSearchActive + is RoomSelectEvent.UpdateVisibleRange -> coroutineScope.launch { dataSource.updateVisibleRange(event.range) } } @@ -89,6 +90,7 @@ class RoomSelectPresenter( return RoomSelectState( mode = mode, + maxNumberOfRooms = maxNumberOfRooms, resultState = searchResults, searchQuery = queryState, isSearchActive = isSearchActive, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt index 927a1c02d6..aadb7c10d1 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectState.kt @@ -16,9 +16,12 @@ import kotlinx.collections.immutable.ImmutableList data class RoomSelectState( val mode: RoomSelectMode, + val maxNumberOfRooms: Int, val resultState: SearchBarResultState>, val searchQuery: TextFieldState, val isSearchActive: Boolean, val selectedRooms: ImmutableList, - val eventSink: (RoomSelectEvents) -> Unit -) + val eventSink: (RoomSelectEvent) -> Unit, +) { + val canSelectMoreRooms = selectedRooms.size < maxNumberOfRooms +} diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt index b0d18659b6..131e35429e 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt @@ -40,18 +40,26 @@ open class RoomSelectStateProvider : PreviewParameterProvider { mode = RoomSelectMode.Share, resultState = SearchBarResultState.Results(aRoomSelectRoomList()), ), + aRoomSelectState( + mode = RoomSelectMode.Share, + resultState = SearchBarResultState.Results(aRoomSelectRoomList()), + selectedRooms = aRoomSelectRoomList().subList(0, 1), + maxNumberOfRooms = 1, + ), ) } internal fun aRoomSelectState( mode: RoomSelectMode = RoomSelectMode.Forward, + maxNumberOfRooms: Int = 10, resultState: SearchBarResultState> = SearchBarResultState.Initial(), searchQuery: String = "", isSearchActive: Boolean = false, selectedRooms: ImmutableList = persistentListOf(), - eventSink: (RoomSelectEvents) -> Unit = {}, + eventSink: (RoomSelectEvent) -> Unit = {}, ) = RoomSelectState( mode = mode, + maxNumberOfRooms = maxNumberOfRooms, resultState = resultState, searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 69f091ddc4..508fcd4fc2 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -44,8 +44,8 @@ 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.Checkbox import io.element.android.libraries.designsystem.theme.components.HorizontalDivider -import io.element.android.libraries.designsystem.theme.components.RadioButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -71,26 +71,30 @@ fun RoomSelectView( onSubmit: (List) -> Unit, modifier: Modifier = Modifier, ) { - @Suppress("UNUSED_PARAMETER") fun onRoomRemoved(roomInfo: SelectRoomInfo) { - // TODO toggle selection when multi-selection is enabled - state.eventSink(RoomSelectEvents.RemoveSelectedRoom) + state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomInfo)) } @Composable - fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList) { - if (isForwarding) return - SelectedRooms( - selectedRooms = selectedRooms, - onRemoveRoom = ::onRoomRemoved, - modifier = Modifier.padding(vertical = 16.dp) - ) + fun SelectedRoomsHelper( + selectedRooms: ImmutableList, + showVerticalSpace: Boolean, + ) { + if (selectedRooms.isNotEmpty()) { + SelectedRooms( + selectedRooms = selectedRooms, + onRemoveRoom = ::onRoomRemoved, + modifier = Modifier.padding(vertical = 16.dp) + ) + } else if (showVerticalSpace) { + Spacer(modifier = Modifier.height(20.dp)) + } } var canHandleBack by remember { mutableStateOf(true) } fun onBackButton(state: RoomSelectState) { if (state.isSearchActive) { - state.eventSink(RoomSelectEvents.ToggleSearchActive) + state.eventSink(RoomSelectEvent.ToggleSearchActive) } else if (canHandleBack) { canHandleBack = false onDismiss() @@ -104,7 +108,7 @@ fun RoomSelectView( val lazyListState = rememberLazyListState() OnVisibleRangeChangeEffect(lazyListState) { visibleRange -> - state.eventSink(RoomSelectEvents.UpdateVisibleRange(visibleRange)) + state.eventSink(RoomSelectEvent.UpdateVisibleRange(visibleRange)) } Scaffold( @@ -141,16 +145,15 @@ fun RoomSelectView( placeHolderTitle = stringResource(CommonStrings.action_search), queryState = state.searchQuery, active = state.isSearchActive, - onActiveChange = { state.eventSink(RoomSelectEvents.ToggleSearchActive) }, + onActiveChange = { state.eventSink(RoomSelectEvent.ToggleSearchActive) }, resultState = state.resultState, showBackButton = false, ) { summaries -> LazyColumn(state = lazyListState) { item { SelectedRoomsHelper( - // TODO state.isForwarding - isForwarding = false, - selectedRooms = state.selectedRooms + selectedRooms = state.selectedRooms, + showVerticalSpace = false, ) } items(summaries, key = { it.roomId.value }) { roomSummary -> @@ -159,8 +162,9 @@ fun RoomSelectView( roomSummary, isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, onSelection = { roomSummary -> - state.eventSink(RoomSelectEvents.SetSelectedRoom(roomSummary)) - } + state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary)) + }, + canBeSelected = state.canSelectMoreRooms, ) HorizontalDivider(modifier = Modifier.fillMaxWidth()) } @@ -169,13 +173,10 @@ fun RoomSelectView( } if (!state.isSearchActive) { - // TODO restore for multi-selection -// SelectedRoomsHelper( -// isForwarding = state.isForwarding, -// selectedRooms = state.selectedRooms -// ) - Spacer(modifier = Modifier.height(20.dp)) - + SelectedRoomsHelper( + selectedRooms = state.selectedRooms, + showVerticalSpace = true, + ) if (state.resultState is SearchBarResultState.Results) { LazyColumn(state = lazyListState) { items(state.resultState.results, key = { it.roomId.value }) { roomSummary -> @@ -184,8 +185,9 @@ fun RoomSelectView( roomSummary, isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, onSelection = { roomSummary -> - state.eventSink(RoomSelectEvents.SetSelectedRoom(roomSummary)) - } + state.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomSummary)) + }, + canBeSelected = state.canSelectMoreRooms, ) HorizontalDivider(modifier = Modifier.fillMaxWidth()) } @@ -218,6 +220,7 @@ private fun SelectedRooms( private fun RoomSummaryView( roomInfo: SelectRoomInfo, isSelected: Boolean, + canBeSelected: Boolean, onSelection: (SelectRoomInfo) -> Unit, ) { Row( @@ -262,7 +265,11 @@ private fun RoomSummaryView( ) } } - RadioButton(selected = isSelected, onClick = { onSelection(roomInfo) }) + Checkbox( + checked = isSelected, + enabled = isSelected || canBeSelected, + onCheckedChange = { onSelection(roomInfo) }, + ) } } diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt index a99a0396c2..6863c1ce20 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/DefaultRoomSelectEntryPointTest.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.roomselect.impl import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.RoomId @@ -17,14 +16,13 @@ import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode +import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DefaultRoomSelectEntryPointTest { +class DefaultRoomSelectEntryPointTest : RobolectricTest() { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @@ -33,13 +31,15 @@ class DefaultRoomSelectEntryPointTest { fun `test node builder`() = runTest { val entryPoint = DefaultRoomSelectEntryPoint() val testMode = RoomSelectMode.Share + val testMaxNumberOfRooms = 12 val parentNode = TestParentNode.create { buildContext, plugins -> RoomSelectNode( buildContext = buildContext, plugins = plugins, - presenterFactory = { mode -> + presenterFactory = { mode, maxNumberOfRooms -> assertThat(mode).isEqualTo(testMode) - createRoomSelectPresenter(mode) + assertThat(maxNumberOfRooms).isEqualTo(testMaxNumberOfRooms) + createRoomSelectPresenter(mode, maxNumberOfRooms) }, ) } @@ -47,7 +47,10 @@ class DefaultRoomSelectEntryPointTest { override fun onRoomSelected(roomIds: List) = lambdaError() override fun onCancel() = lambdaError() } - val params = RoomSelectEntryPoint.Params(testMode) + val params = RoomSelectEntryPoint.Params( + mode = testMode, + maxNumberOfRooms = testMaxNumberOfRooms, + ) val result = entryPoint.createNode( parentNode = parentNode, buildContext = BuildContext.root(null), @@ -55,7 +58,7 @@ class DefaultRoomSelectEntryPointTest { callback = callback, ) assertThat(result).isInstanceOf(RoomSelectNode::class.java) - assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode)) + assertThat(result.plugins).contains(RoomSelectNode.Inputs(params.mode, params.maxNumberOfRooms)) assertThat(result.plugins).contains(callback) } } diff --git a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt index 67c1b52231..b5a4ccfd12 100644 --- a/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt +++ b/libraries/roomselect/impl/src/test/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenterTest.kt @@ -9,9 +9,6 @@ package io.element.android.libraries.roomselect.impl import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.roomlist.RoomListFilter @@ -20,10 +17,12 @@ import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo +import io.element.android.libraries.roomselect.api.RoomSelectEntryPoint import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope @@ -40,26 +39,24 @@ class RoomSelectPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = createRoomSelectPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.selectedRooms).isEmpty() assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.Initial::class.java) assertThat(initialState.isSearchActive).isFalse() + assertThat(initialState.maxNumberOfRooms).isEqualTo(10) + assertThat(initialState.canSelectMoreRooms).isTrue() } } @Test fun `present - toggle search active`() = runTest { val presenter = createRoomSelectPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() - initialState.eventSink(RoomSelectEvents.ToggleSearchActive) + initialState.eventSink(RoomSelectEvent.ToggleSearchActive) assertThat(awaitItem().isSearchActive).isTrue() - initialState.eventSink(RoomSelectEvents.ToggleSearchActive) + initialState.eventSink(RoomSelectEvent.ToggleSearchActive) assertThat(awaitItem().isSearchActive).isFalse() } } @@ -76,15 +73,13 @@ class RoomSelectPresenterTest { val presenter = createRoomSelectPresenter( roomListService = roomListService ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() val expectedRoomInfo = roomSummary.toSelectRoomInfo() // Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result val result = (awaitItem().resultState as SearchBarResultState.Results).results assertThat(result).isEqualTo(listOf(expectedRoomInfo)) - initialState.eventSink(RoomSelectEvents.ToggleSearchActive) + initialState.eventSink(RoomSelectEvent.ToggleSearchActive) skipItems(1) initialState.searchQuery.setTextAndPlaceCursorAtEnd("string not contained") assertThat( @@ -110,17 +105,22 @@ class RoomSelectPresenterTest { createRoomListLambda = { roomList } ) val presenter = createRoomSelectPresenter( + maxNumberOfRooms = 1, roomListService = roomListService, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() val roomInfo = roomSummary.toSelectRoomInfo() - initialState.eventSink(RoomSelectEvents.SetSelectedRoom(roomInfo)) - assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(roomInfo)) - initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom) - assertThat(awaitItem().selectedRooms).isEmpty() + initialState.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomInfo)) + awaitItem().let { + assertThat(it.selectedRooms).isEqualTo(persistentListOf(roomInfo)) + assertThat(it.canSelectMoreRooms).isFalse() + it.eventSink(RoomSelectEvent.ToggleSelectedRoom(roomInfo)) + } + awaitItem().let { + assertThat(it.selectedRooms).isEmpty() + assertThat(it.canSelectMoreRooms).isTrue() + } cancel() } } @@ -136,9 +136,7 @@ class RoomSelectPresenterTest { createRoomListLambda = { roomList } ) val presenter = createRoomSelectPresenter(roomListService = roomListService) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() // Post some rooms to simulate loaded content val rooms = (1..10).map { aRoomSummary() } @@ -146,7 +144,7 @@ class RoomSelectPresenterTest { skipItems(1) // UpdateVisibleRange near end should trigger loadMore - initialState.eventSink(RoomSelectEvents.UpdateVisibleRange(IntRange(0, 9))) + initialState.eventSink(RoomSelectEvent.UpdateVisibleRange(IntRange(0, 9))) // Give time for the coroutine to complete testScheduler.advanceUntilIdle() @@ -157,9 +155,11 @@ class RoomSelectPresenterTest { internal fun TestScope.createRoomSelectPresenter( mode: RoomSelectMode = RoomSelectMode.Forward, + maxNumberOfRooms: Int = RoomSelectEntryPoint.DEFAULT_MAX_NUMBER_OF_ROOMS, roomListService: RoomListService = FakeRoomListService(), ) = RoomSelectPresenter( mode = mode, + maxNumberOfRooms = maxNumberOfRooms, dataSourceFactory = object : RoomSelectSearchDataSource.Factory { override fun create(coroutineScope: CoroutineScope): RoomSelectSearchDataSource { return RoomSelectSearchDataSource( diff --git a/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt b/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt index 38abed0d92..b70413ffe5 100644 --- a/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt +++ b/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt @@ -14,7 +14,6 @@ package org.rustls.platformverifier import android.annotation.SuppressLint import android.content.Context import android.net.http.X509TrustManagerExtensions -import android.os.Build import android.util.Log import java.io.ByteArrayInputStream import java.io.File @@ -22,18 +21,13 @@ import java.security.KeyStore import java.security.KeyStoreException import java.security.MessageDigest import java.security.PublicKey -import java.security.cert.CertPathValidator -import java.security.cert.CertPathValidatorException import java.security.cert.CertificateException import java.security.cert.CertificateExpiredException import java.security.cert.CertificateFactory import java.security.cert.CertificateNotYetValidException import java.security.cert.CertificateParsingException -import java.security.cert.PKIXBuilderParameters -import java.security.cert.PKIXRevocationChecker import java.security.cert.X509Certificate import java.util.Date -import java.util.EnumSet import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import javax.security.auth.x500.X500Principal @@ -298,90 +292,14 @@ internal object CertificateVerifier { return VerificationResult(StatusCode.Ok) } - // Try to check the revocation status of the cert, if it is supported. + // ================================================================================================= + // IMPORTANT! DIFF WITH THE ORIGINAL SOURCES HERE: // - // This is supported at >= API 24, but we're supporting 22 (Android 5) for the best - // compatibility. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - // Note: - // - // 1. Android does not provide any way only to attempt to validate revocation from cached - // data like the other platforms do. This means it will always use the network for - // certificates which had no stapled response. - // - // 2: Likely because of 1, Android requires all issued certificates to have some form of - // revocation included in their authority information. This doesn't work universally as - // issuing certificates in use may omit authority access information (for example the - // Let's Encrypt R3 Intermediate Certificate). - // - // Given these constraints, the best option is to only check revocation information - // at the end-entity depth. We will prefer OCSP (to use stapled information if possible). - // If there is no stapled OCSP response, Android may use the network to attempt to fetch - // one. If OCSP checking fails, it may fall back to fetching CRLs. We allow "soft" - // failures, for example transient network errors. - // - // In the case of a non-public root, such as an internal CA or self-signed certificate, - // we opt to skip revocation checks entirely. The only exception is if the server - // provided stapled OCSP data, which is an explicit signal and won't introduce non-ideal - // platform behavior when attempting validation. - // - // This is because these are cases where a user or administrator has explicitly opted to - // trust a certificate they (at least believe) have control over. These certificates rarely - // contain revocation information as well, so these cases don't lose much. - // See https://github.com/rustls/rustls-platform-verifier/issues/69 as well. - if (ocspResponse == null && !isKnownRoot(validChain.last())) { - // Chain validation must have succeeded by this point. - return VerificationResult(StatusCode.Ok) - } - - val parameters = PKIXBuilderParameters(keystore, null) - - val validator = CertPathValidator.getInstance("PKIX") - val revocationChecker = validator.revocationChecker as PKIXRevocationChecker - - revocationChecker.options = EnumSet.of( - PKIXRevocationChecker.Option.SOFT_FAIL, - PKIXRevocationChecker.Option.ONLY_END_ENTITY - ) - - // Use the OCSP data `rustls` provided, if present. - // Its expected that the server only sends revocation data for its own leaf certificate. - // - // If this field is set, then Android will use it and skip any networking to - // attempt a fetch for that certificate. Otherwise, it will attempt to fetch it from the network. - // Ref: https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/security/provider/certpath/RevocationChecker.java;l=694 - ocspResponse?.let { providedResponse -> - revocationChecker.ocspResponses = mapOf(endEntity to providedResponse) - } - - // Use the custom revocation definition. - // "Note that when a `PKIXRevocationChecker` is added to `PKIXParameters`, it clones the `PKIXRevocationChecker`; - // thus any subsequent modifications to the `PKIXRevocationChecker` have no effect." - // - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker - parameters.certPathCheckers = listOf(revocationChecker) - // "When supplying a revocation checker in this manner, it will be used to check revocation - // irrespective of the setting of the `RevocationEnabled` flag." - // - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker - parameters.isRevocationEnabled = false - - // Validate the revocation status of the end entity certificate. - try { - validator.validate(certFactory.generateCertPath(validChain), parameters) - } catch (e: CertPathValidatorException) { - // LetsEncrypt no longer include OCSP information (as OCSP is being deprecated) which Android is not - // happy with since it *only* tries OCSP by default. We aren't 100% decided on how to fix this yet for real - // (see https://github.com/rustls/rustls-platform-verifier/pull/179) so for now we implement an out for - // tests to allow regular maintenance to proceed. - if (BuildConfig.TEST && e.reason == CertPathValidatorException.BasicReason.UNSPECIFIED) { - return VerificationResult(StatusCode.Ok) - } - - return VerificationResult(StatusCode.Revoked, e.toString()) - } - } else { - // This is allowed to be skipped since revocation checking is best-effort. - Log.w(TAG, "did not attempt to validate OCSP due to Android version") - } + // We removed the cert revocation checks because Android won't do them by default + // and supporting them would be quite difficult given CRLs won't work out of the box. + // + // See https://github.com/rustls/rustls-platform-verifier/issues/221 for more info. + // ================================================================================================= return VerificationResult(StatusCode.Ok) } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index 568dbe7e3a..88cf9558b3 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -24,8 +24,8 @@ data class SessionData( val refreshToken: String?, /** The homeserver URL of the session. */ val homeserverUrl: String, - /** The Open ID Connect info for this session, if any. */ - val oidcData: String?, + /** The Open Authorization info for this session, if any. */ + val oAuthData: String?, /** The timestamp of the last login. May be `null` in very old sessions. */ val loginTimestamp: Date?, /** Whether the [accessToken] is valid or not. */ diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index ea69709bbd..316bf86c1c 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -20,7 +20,7 @@ internal fun SessionData.toDbModel(): DbSessionData { accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl, - oidcData = oidcData, + oidcData = oAuthData, loginTimestamp = loginTimestamp?.time, isTokenValid = if (isTokenValid) 1L else 0L, loginType = loginType.name, @@ -41,7 +41,7 @@ internal fun DbSessionData.toApiModel(): SessionData { accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl, - oidcData = oidcData, + oAuthData = oidcData, loginTimestamp = loginTimestamp?.let { Date(it) }, isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt index fb2c9a2c78..8955aa4a27 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt @@ -17,7 +17,9 @@ import dev.zacsweers.metro.SingleIn import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.sessionstorage.impl.SessionDatabase import io.element.encrypteddb.SqlCipherDriverFactory -import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider +import io.element.encrypteddb.passphrase.RandomDatabaseSecretProvider +import io.element.encrypteddb.utils.ReplaceDatabaseKey +import timber.log.Timber @BindingContainer @ContributesTo(AppScope::class) @@ -36,9 +38,20 @@ object SessionStorageModule { parentDir.mkdirs() } - val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile) + val rekeyMigrationVersion = 11L + val passphraseProvider = RandomDatabaseSecretProvider(context, secretFile) val driver = SqlCipherDriverFactory(passphraseProvider) - .create(SessionDatabase.Schema, "$name.db", context) + .create( + schema = SessionDatabase.Schema, + name = "$name.db", + context = context, + ) { db, oldVersion, newVersion -> + Timber.d("Migrating $name database from version $oldVersion to $newVersion") + if (rekeyMigrationVersion in oldVersion..newVersion) { + ReplaceDatabaseKey(passphraseProvider).replaceKey(name, db) + } + } + return SessionDatabase(driver) } } diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/11.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/11.sqm new file mode 100644 index 0000000000..3d61c92c07 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/11.sqm @@ -0,0 +1,2 @@ +-- Migrate DB from version 11 +-- Dummy migration to trigger a re-key diff --git a/libraries/session-storage/test/build.gradle.kts b/libraries/session-storage/test/build.gradle.kts index cfdc3018a9..7a89746812 100644 --- a/libraries/session-storage/test/build.gradle.kts +++ b/libraries/session-storage/test/build.gradle.kts @@ -14,6 +14,7 @@ android { } dependencies { + implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.sessionStorage.api) } diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt index c791a20620..c5acd77755 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt @@ -30,7 +30,7 @@ fun aSessionData( accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = "aHomeserverUrl", - oidcData = null, + oAuthData = null, loginTimestamp = null, isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/observer/FakeSessionObserver.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/observer/FakeSessionObserver.kt index fdf5cc5f1b..a7c3a6837b 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/observer/FakeSessionObserver.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/observer/FakeSessionObserver.kt @@ -10,12 +10,13 @@ package io.element.android.libraries.sessionstorage.test.observer import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import java.util.concurrent.CopyOnWriteArraySet class FakeSessionObserver : SessionObserver { - private val _listeners = mutableListOf() + private val _listeners = CopyOnWriteArraySet() val listeners: List - get() = _listeners + get() = _listeners.toList() override fun addListener(listener: SessionListener) { _listeners.add(listener) diff --git a/libraries/slashcommands/impl/build.gradle.kts b/libraries/slashcommands/impl/build.gradle.kts index 34dc2e42b2..8e10b09b71 100644 --- a/libraries/slashcommands/impl/build.gradle.kts +++ b/libraries/slashcommands/impl/build.gradle.kts @@ -18,6 +18,7 @@ android { setupDependencyInjection() dependencies { + implementation(libs.coroutines.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.preferences.api) implementation(projects.libraries.core) 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 index 0d9e1e8c72..5df871e361 100644 --- 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 @@ -109,7 +109,7 @@ enum class Command( parameters = "", description = R.string.slash_command_description_nick_for_room, isAllowedInThread = false, - isSupported = false, + isSupported = true, ), ROOM_AVATAR( command = "/roomavatar", 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 index ad252cb224..01bac5825b 100644 --- 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 @@ -47,7 +47,7 @@ class CommandExecutor( is SlashCommand.ChangeAvatar -> changeAvatar() is SlashCommand.ChangeAvatarForRoom -> changeAvatarForRoom() is SlashCommand.ChangeDisplayName -> changeDisplayName(slashCommand) - is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom() + is SlashCommand.ChangeDisplayNameForRoom -> changeDisplayNameForRoom(slashCommand) is SlashCommand.ChangeRoomAvatar -> changeRoomAvatar() is SlashCommand.ChangeRoomName -> changeRoomName(slashCommand) is SlashCommand.ChangeTopic -> changeTopic(slashCommand) @@ -171,8 +171,8 @@ class CommandExecutor( return Result.failure(Exception("Not yet implemented")) } - private fun changeDisplayNameForRoom(): Result { - return Result.failure(Exception("Not yet implemented")) + private suspend fun changeDisplayNameForRoom(slashCommand: SlashCommand.ChangeDisplayNameForRoom): Result { + return joinedRoom.setOwnMemberDisplayName(slashCommand.displayName) } private suspend fun changeDisplayName(slashCommand: SlashCommand.ChangeDisplayName): Result { 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 index 497f45c96f..c0f2ce89c2 100644 --- 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 @@ -14,7 +14,6 @@ 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 @@ -185,10 +184,18 @@ class CommandExecutorTest { } @Test - fun `change display name for room is not supported`() = runTest { - val sut = createCommandExecutor() - val res = sut.proceedAdmin(SlashCommand.ChangeDisplayNameForRoom(A_USER_NAME)) - assertThat(res.isFailure).isTrue() + fun `change display name for room delegates to joined room`() = runTest { + var capturedDisplayName: String? = null + val joinedRoom = FakeJoinedRoom( + setOwnMemberDisplayNameResult = { displayName -> + capturedDisplayName = displayName + Result.success(Unit) + } + ) + val sut = createCommandExecutor(joinedRoom = joinedRoom) + val res = sut.proceedAdmin(SlashCommand.ChangeDisplayNameForRoom("room nick")) + assertThat(res.isSuccess).isTrue() + assertThat(capturedDisplayName).isEqualTo("room nick") } @Test diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index e564ddac41..593560936e 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -29,6 +29,8 @@ object TestTags { * Verification screen. */ val recoveryKey = TestTag("verification-recovery_key") + val customRecoveryPassphrase = TestTag("verification-custom_recovery_passphrase") + val customRecoveryPassphraseConfirm = TestTag("verification-custom_recovery_passphrase_confirm") /** * Sign out screen. @@ -124,4 +126,6 @@ object TestTags { * */ val roomAddressField = TestTag("room_address_field") + + val confirmInviteUnknown = TestTag("confirm_invite_unknown") } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 4860a53ae7..0f62eb5234 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -419,7 +420,7 @@ fun TextComposer( // Re-focus the text input when voice recording ends so the user can continue typing var previousVoiceMessageState by remember { mutableStateOf(voiceMessageState) } - LaunchedEffect(voiceMessageState) { + LaunchedEffect(voiceMessageState, onRequestFocus) { if (voiceMessageState is VoiceMessageState.Idle && previousVoiceMessageState !is VoiceMessageState.Idle) { onRequestFocus() } @@ -522,6 +523,7 @@ private fun StandardLayout( .padding(bottom = 8.dp, top = 8.dp) .weight(1f) ) { + val movableVoiceRecording = remember { movableContentOf { voiceRecording() } } if (voiceMessageState is VoiceMessageState.Idle) { textInput() } else if (composerMode is MessageComposerMode.Special) { @@ -530,10 +532,10 @@ private fun StandardLayout( onResetComposerMode = onResetComposerMode, isTextEmpty = true, ) { - voiceRecording() + movableVoiceRecording() } } else { - voiceRecording() + movableVoiceRecording() } } // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. @@ -679,7 +681,7 @@ private fun TextInputBox( .align(Alignment.CenterEnd), imageVector = CompoundIcons.InfoSolid(), tint = ElementTheme.colors.iconCriticalPrimary, - contentDescription = null, + contentDescription = stringResource(CommonStrings.a11y_info), ) if (showBottomSheet) { CaptionWarningBottomSheet( diff --git a/libraries/textcomposer/impl/src/main/res/values-be/translations.xml b/libraries/textcomposer/impl/src/main/res/values-be/translations.xml index 7f5693b0a4..dae17ce795 100644 --- a/libraries/textcomposer/impl/src/main/res/values-be/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-be/translations.xml @@ -4,7 +4,9 @@ "Пераключыць маркіраваны спіс" "Закрыць параметры фарматавання" "Пераключыць блок кода" + "Зашыфраванае паведамленне…" "Паведамленне…" + "Незашыфраванае паведамленне…" "Стварыць спасылку" "Рэдагаваць спасылку" "Ужыць тоўсты шрыфт" diff --git a/libraries/textcomposer/impl/src/main/res/values-ca/translations.xml b/libraries/textcomposer/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..f3b78b9dd2 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,31 @@ + + + "Afegeix fitxer adjunt" + "Activa/desactiva la llista amb punts" + "Cancel·la i tanca el format de text" + "Activa/desactiva bloc de codi" + "Afegeix llegenda" + "Missatge xifrat…" + "Missatge…" + "Missatge no xifrat…" + "Crea enllaç" + "Edita enllaç" + "%1$s, estat: %2$s" + "Aplica el format negreta" + "Aplica el format cursiva" + "desactivat" + "Aplica el format ratllat" + "Aplica el format subratllat" + "Activa/desactiva mode pantalla completa" + "Sagnia" + "Aplica format de codi" + "Estableix enllaç" + "Activa/desactiva llista numerada" + "Obre les opcions de redacció" + "Activa/desactiva cometes" + "Elimina enllaç" + "Treu sagnia" + "Enllaç" + "És possible que les llegendes no siguin visibles pels que utilitzin aplicacions antigues." + "Mantén premut per gravar." + diff --git a/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml b/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml index fdde227bef..f79cb2f30c 100644 --- a/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-zh/translations.xml @@ -4,8 +4,8 @@ "切换符号列表" "取消并关闭文本格式" "切换代码块" - "可选的标题……" - "加密信息…" + "添加标题…" + "已加密的消息…" "消息…" "未加密的消息…" "创建链接" diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt index 9a65ca0ad5..ba49428cae 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/components/markdown/MarkdownTextInputTest.kt @@ -6,14 +6,16 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.textcomposer.impl.components.markdown import android.widget.EditText import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import androidx.core.text.getSpans -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.matrix.api.permalink.PermalinkData @@ -32,66 +34,53 @@ import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState import io.element.android.tests.testutils.EnsureCalledOnceWithParam import io.element.android.tests.testutils.EventsRecorder -import kotlinx.coroutines.test.runTest -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class MarkdownTextInputTest { - @get:Rule val rule = createAndroidComposeRule() +class MarkdownTextInputTest : RobolectricTest() { @Test - fun `when user types onTyping is triggered with value 'true'`() = runTest { + fun `when user types onTyping is triggered with value 'true'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onTyping = EnsureCalledOnceWithParam(expectedParam = true, result = Unit) - rule.setMarkdownTextInput(state = state, onTyping = onTyping) - rule.activityRule.scenario.onActivity { - it.findEditor().setText("Test") - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onTyping = onTyping) + activity!!.findEditor().setText("Test") + awaitIdle() onTyping.assertSuccess() } @Test - fun `when user removes text onTyping is triggered with value 'false'`() = runTest { + fun `when user removes text onTyping is triggered with value 'false'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onTyping = EventsRecorder() - rule.setMarkdownTextInput(state = state, onTyping = onTyping) - rule.activityRule.scenario.onActivity { - val editText = it.findEditor() - editText.setText("Test") - editText.setText("") - editText.setText(null) - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onTyping = onTyping) + val editText = activity!!.findEditor() + editText.setText("Test") + editText.setText("") + editText.setText(null) + awaitIdle() onTyping.assertList(listOf(true, false, false)) } @Test - fun `when user types something that's not a mention onSuggestionReceived is triggered with 'null'`() = runTest { + fun `when user types something that's not a mention onSuggestionReceived is triggered with 'null'`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onSuggestionReceived = EventsRecorder() - rule.setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) - rule.activityRule.scenario.onActivity { - it.findEditor().setText("Test") - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) + activity!!.findEditor().setText("Test") + awaitIdle() onSuggestionReceived.assertSingle(null) } @Test - fun `when user types something that's a mention onSuggestionReceived is triggered a real value`() = runTest { + fun `when user types something that's a mention onSuggestionReceived is triggered a real value`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialFocus = true) val onSuggestionReceived = EventsRecorder() - rule.setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) - rule.activityRule.scenario.onActivity { - it.findEditor().setText("@") - it.findEditor().setText("#") - it.findEditor().setText("/") - } - rule.awaitIdle() + setMarkdownTextInput(state = state, onSuggestionReceived = onSuggestionReceived) + val editor = activity!!.findEditor() + editor.setText("@") + editor.setText("#") + editor.setText("/") + awaitIdle() onSuggestionReceived.assertList( listOf( // User mention suggestion @@ -105,69 +94,59 @@ class MarkdownTextInputTest { } @Test - fun `when the selection changes in the UI the state is updated`() = runTest { + fun `when the selection changes in the UI the state is updated`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = true) - rule.setMarkdownTextInput(state = state) - rule.activityRule.scenario.onActivity { - val editor = it.findEditor() - editor.setSelection(2) - } - rule.awaitIdle() + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + editor.setSelection(2) + awaitIdle() // Selection is updated assertThat(state.selection).isEqualTo(2..2) } @Test - fun `when the selection state changes in the view is updated`() = runTest { + fun `when the selection state changes in the view is updated`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = true) - rule.setMarkdownTextInput(state = state) - var editor: EditText? = null - rule.activityRule.scenario.onActivity { - editor = it.findEditor() - state.selection = 2..2 - } - rule.awaitIdle() + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + state.selection = 2..2 + awaitIdle() // Selection state is updated - assertThat(editor?.selectionStart).isEqualTo(2) - assertThat(editor?.selectionEnd).isEqualTo(2) + assertThat(editor.selectionStart).isEqualTo(2) + assertThat(editor.selectionEnd).isEqualTo(2) } @Test - fun `when the view focus changes the state is updated`() = runTest { + fun `when the view focus changes the state is updated`() = runAndroidComposeUiTest { val state = aMarkdownTextEditorState(initialText = "Test", initialFocus = false) - rule.setMarkdownTextInput(state = state) - rule.activityRule.scenario.onActivity { - val editor = it.findEditor() - editor.requestFocus() - } + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + editor.requestFocus() // Focus state is updated assertThat(state.hasFocus).isTrue() } @Test - fun `inserting a mention replaces the existing text with a span`() = runTest { + fun `inserting a mention replaces the existing text with a span`() = runAndroidComposeUiTest { val permalinkParser = FakePermalinkParser(result = { PermalinkData.UserLink(A_SESSION_ID) }) val state = aMarkdownTextEditorState(initialText = "@", initialFocus = true) state.currentSuggestion = Suggestion(0, 1, SuggestionType.Mention, "") - rule.setMarkdownTextInput(state = state) - var editor: EditText? = null - rule.activityRule.scenario.onActivity { - editor = it.findEditor() - state.insertSuggestion( - ResolvedSuggestion.Member(roomMember = aRoomMember()), - aMentionSpanProvider(permalinkParser), - ) - } - rule.awaitIdle() + setMarkdownTextInput(state = state) + val editor = activity!!.findEditor() + state.insertSuggestion( + ResolvedSuggestion.Member(roomMember = aRoomMember()), + aMentionSpanProvider(permalinkParser), + ) + awaitIdle() // Text is replaced with a placeholder - assertThat(editor?.editableText.toString()).isEqualTo("@ ") + assertThat(editor.editableText.toString()).isEqualTo("@ ") // The placeholder contains a MentionSpan - val mentionSpans = editor?.editableText?.getSpans(0, 2).orEmpty() + val mentionSpans = editor.editableText?.getSpans(0, 2).orEmpty() assertThat(mentionSpans).isNotEmpty() } - private fun AndroidComposeTestRule.setMarkdownTextInput( + private fun AndroidComposeUiTest.setMarkdownTextInput( state: MarkdownTextEditorState = aMarkdownTextEditorState(), onTyping: (Boolean) -> Unit = {}, onSuggestionReceived: (Suggestion?) -> Unit = {}, diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt index 7bac75cc75..0eec4805a9 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/IntentionalMentionSpanProviderTest.kt @@ -17,13 +17,11 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.textcomposer.mentions.MentionType import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) -class IntentionalMentionSpanProviderTest { +class IntentionalMentionSpanProviderTest : RobolectricTest() { @JvmField @Rule val warmUpRule = WarmUpRule() 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 6e57ce68cb..247a74a0eb 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 @@ -11,7 +11,6 @@ package io.element.android.libraries.textcomposer.impl.model import android.net.Uri import androidx.core.text.buildSpannedString import androidx.core.text.inSpans -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.UserId @@ -31,11 +30,10 @@ import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.aMarkdownTextEditorState +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class MarkdownTextEditorStateTest { +class MarkdownTextEditorStateTest : RobolectricTest() { @Test fun `insertMention - room alias - getMentions return empty list`() { val state = aMarkdownTextEditorState(initialText = "Hello @", initialFocus = true) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt index 9ae748d03a..9105653516 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,6 +43,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem +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.ListItem diff --git a/libraries/troubleshoot/impl/src/main/res/values-ca/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..fe85dadb55 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ca/translations.xml @@ -0,0 +1,11 @@ + + + "Executa tests" + "Torna a executar tests" + "Alguns tests han fallat. Comprova\'n els detalls." + "Executa els tests per detectar qualsevol problema a la configuració que pugui fer que les notificacions no es comportin com s\'espera." + "Intenta solucionar" + "Tots els tests s\'han superat amb èxit." + "Resolució de problemes de notificacions" + "Alguns tests necessiten la teva atenció. Comprova\'n els detalls." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml index ff9e80f687..876811c861 100644 --- a/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml +++ b/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml @@ -7,6 +7,6 @@ "Выполните тесты, чтобы выявить проблемы в конфигурации, из-за которой уведомления могут работать не так, как ожидалось." "Попытаться исправить" "Все тесты прошли успешно." - "Уведомления об устранении неполадок" + "Устранение неполадок с уведомлениями" "Некоторые тесты требуют вашего внимания. Пожалуйста, проверьте подробности." diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt index 0ba6c22710..ea98aeabc6 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt @@ -6,60 +6,56 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.troubleshoot.impl import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBack -import org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith import org.robolectric.annotation.Config -@RunWith(AndroidJUnit4::class) -class TroubleshootNotificationsViewTest { - @get:Rule - val rule = createAndroidComposeRule() - +class TroubleshootNotificationsViewTest : RobolectricTest() { @Test - fun `press menu back invokes the expected callback`() { + fun `press menu back invokes the expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( state = aTroubleshootNotificationsState( eventSink = eventsRecorder ), onBackClick = it, ) - rule.pressBack() + pressBack() } } @Test - fun `clicking on run test emits the expected Event`() { + fun `clicking on run test emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( aTroubleshootNotificationsState( eventSink = eventsRecorder ), ) - rule.onNodeWithText("Run tests").performClick() + onNodeWithText("Run tests").performClick() eventsRecorder.assertSingle(TroubleshootNotificationsEvents.StartTests) } @Config(qualifiers = "h1024dp") @Test - fun `clicking on run test again emits the expected Event`() { + fun `clicking on run test again emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( aTroubleshootTestStateFailure( @@ -69,7 +65,7 @@ class TroubleshootNotificationsViewTest { eventSink = eventsRecorder ), ) - rule.onNodeWithText("Run tests again").performClick() + onNodeWithText("Run tests again").performClick() eventsRecorder.assertList( listOf( TroubleshootNotificationsEvents.RetryFailedTests, @@ -80,9 +76,9 @@ class TroubleshootNotificationsViewTest { @Config(qualifiers = "h1024dp") @Test - fun `clicking on quick fix emits the expected Event`() { + fun `clicking on quick fix emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setTroubleshootNotificationsView( + setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( aTroubleshootTestStateFailure( @@ -92,7 +88,7 @@ class TroubleshootNotificationsViewTest { eventSink = eventsRecorder ), ) - rule.onNodeWithText("Attempt to fix").performClick() + onNodeWithText("Attempt to fix").performClick() eventsRecorder.assertList( listOf( TroubleshootNotificationsEvents.RetryFailedTests, @@ -102,7 +98,7 @@ class TroubleshootNotificationsViewTest { } } -private fun AndroidComposeTestRule.setTroubleshootNotificationsView( +private fun AndroidComposeUiTest.setTroubleshootNotificationsView( state: TroubleshootNotificationsState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt index fa4e65ad9a..b9f615d68e 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt @@ -6,15 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.libraries.troubleshoot.impl.history import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi 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 androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_FORMATTED_DATE import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -23,67 +25,61 @@ 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 org.junit.Rule +import io.element.android.tests.testutils.robolectric.RobolectricTest import org.junit.Test -import org.junit.rules.TestRule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PushHistoryViewTest { - @get:Rule - val rule = createAndroidComposeRule() +class PushHistoryViewTest : RobolectricTest() { @Test - fun `clicking on Reset sends a PushHistoryEvents`() { + fun `clicking on Reset sends a PushHistoryEvents`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( pushCounter = 123, eventSink = eventsRecorder, ), ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.clickOn(CommonStrings.action_reset) + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + clickOn(CommonStrings.action_reset) eventsRecorder.assertSingle(PushHistoryEvents.Reset(requiresConfirmation = true)) // Also check that the push counter is rendered - rule.onNodeWithText("123").assertExists() + onNodeWithText("123").assertExists() } @Test - fun `clicking on show only errors sends a PushHistoryEvents(true)`() { + fun `clicking on show only errors sends a PushHistoryEvents(true)`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( showOnlyErrors = false, eventSink = eventsRecorder, ), ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.onNodeWithText("Show only errors").performClick() + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + onNodeWithText("Show only errors").performClick() eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) } @Test - fun `clicking on show only errors sends a PushHistoryEvents(false)`() { + fun `clicking on show only errors sends a PushHistoryEvents(false)`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( showOnlyErrors = true, eventSink = eventsRecorder, ), ) - val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu) - rule.onNodeWithContentDescription(menuContentDescription).performClick() - rule.onNodeWithText("Show only errors").performClick() + val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) + onNodeWithContentDescription(menuContentDescription).performClick() + onNodeWithText("Show only errors").performClick() eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) } @Test - fun `clicking on an invalid event has no effect`() { + fun `clicking on an invalid event has no effect`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder(expectEvents = false) - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( aPushHistoryItem( @@ -93,14 +89,14 @@ class PushHistoryViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText(A_FORMATTED_DATE).performClick() + onNodeWithText(A_FORMATTED_DATE).performClick() // No callback invoked } @Test - fun `clicking on a valid event emits the expected Event`() { + fun `clicking on a valid event emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() - rule.setPushHistoryView( + setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( aPushHistoryItem( @@ -113,7 +109,7 @@ class PushHistoryViewTest { eventSink = eventsRecorder, ), ) - rule.onNodeWithText(A_FORMATTED_DATE).performClick() + onNodeWithText(A_FORMATTED_DATE).performClick() eventsRecorder.assertSingle( PushHistoryEvents.NavigateTo( sessionId = A_SESSION_ID, @@ -124,7 +120,7 @@ class PushHistoryViewTest { } } -private fun AndroidComposeTestRule.setPushHistoryView( +private fun AndroidComposeUiTest.setPushHistoryView( state: PushHistoryState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { 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 4011474193..511581e672 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -13,6 +13,7 @@ "Поўны адрас будзе %1$s" "Дэталі шыфравання" "Схаваць пароль" + "Інфармацыя" "Далучыцца да выкліку" "Перайсці ўніз" "Толькі згадкі" @@ -72,12 +73,16 @@ "Дэактываваць уліковы запіс" "Адхіліць" "Адхіліць і заблакіраваць" + "Выдаліць" + "Выдаліць уліковы запіс" + "Выдаліць файл" "Выдаліць апытанне" "Зняць выбар з усіх" "Адключыць" "Адмяніць" "Aдхіліць" "Гатова" + "Спампаваць" "Рэдагаваць" "Рэдагаваць подпіс" "Рэдагаваць апытанне" @@ -119,6 +124,7 @@ "Рэакцыя" "Адхіліць" "Выдаліць" + "Выдаліць паведамленне" "Адказаць" "Адказаць у гутаркі" "Паскардзіцца" @@ -133,7 +139,7 @@ "Выбраць усё" "Адправіць" "Адправіць паведамленне" - "Падзяліцца" + "Абагуліць" "Абагуліць спасылку" "Паказаць" "Увайдзіце яшчэ раз" @@ -170,7 +176,6 @@ "Аўтарскае права" "Стварэнне пакоя…" "Выйшаў з пакоя" - "Цёмная" "Памылка расшыфроўкі" "Параметры распрацоўшчыка" "Прамы чат" @@ -197,7 +202,6 @@ "Усталяваць APK" "Гэты Matrix ID не знойдзены, таму запрашэнне можа быць не атрымана." "Пакінуць пакой" - "Светлая" "Спасылка скапіравана ў буфер абмену" "Загрузка…" @@ -216,8 +220,10 @@ "Паведамленне выдалена" "Сучасны" "Адкл. гук" + "Імя" "Вынікаў няма" "Няма назвы пакоя" + "Не зашыфравана" "Па-за сеткай" "Ліцэнзіі з адкрытым зыходным кодам" "або" @@ -229,6 +235,7 @@ "Калі ласка, пачакайце…" "Вы ўпэўнены, што хочаце скончыць гэтае апытанне?" "Апытанне: %1$s" + "Апытанне" "Усяго галасоў: %1$s" "Вынікі будуць паказаны пасля завяршэння апытання" @@ -243,8 +250,8 @@ "Рэакцыі" "Ключ аднаўлення" "Абнаўленне…" + "Выдаленне…" "Адказвае на %1$s" - "Паведаміць пра памылку" "Паведаміць аб праблеме" "Скарга прынята" "Рэдактар фарматаванага тэксту" @@ -265,6 +272,7 @@ "Сервер не падтрымліваецца" "URL-адрас сервера" "Налады" + "Абагуліць прастору" "Абагулена месцазнаходжанне" "Выхад" "Нешта пайшло не так" @@ -273,7 +281,6 @@ "Поспех" "Прапановы" "Сінхранізацыя" - "Сістэмная" "Тэкст" "Паведамленні трэціх асоб" "Гутарка" @@ -319,6 +326,11 @@ "Гэй, пагавары са мной у %1$s: %2$s" "%1$s Android" "Паведаміць аб памылцы з дапамогай Rageshake" + "Здымак экрана" + "%1$s: %2$s" + "Параметры" + "Выдаліць %1$s" + "Налады" "Не ўдалося выбраць носьбіт, паўтарыце спробу." "Націсніце на паведамленне і абярыце «%1$s », каб уключыць сюды." "Замацуеце важныя паведамленні, каб іх можна было лёгка знайсці" @@ -339,8 +351,8 @@ "Загрузка паведамлення…" "Паглядзець усе" "Чат" - "Падзяліцца месцазнаходжаннем" - "Падзяліцца маім месцазнаходжаннем" + "Абагуліць месцазнаходжанне" + "Абагуліць мае месцазнаходжанне" "Адкрыць у Apple Maps" "Адкрыць у Google Maps" "Адкрыць у OpenStreetMap" diff --git a/libraries/ui-strings/src/main/res/values-bg/translations.xml b/libraries/ui-strings/src/main/res/values-bg/translations.xml index 94e050727d..3f38b1991f 100644 --- a/libraries/ui-strings/src/main/res/values-bg/translations.xml +++ b/libraries/ui-strings/src/main/res/values-bg/translations.xml @@ -145,7 +145,6 @@ "Създаване на стая…" "Стаята е напусната" "Пространството е напуснато" - "Тъмен" "Грешка при разшифроване" "Описание" "Опции за разработчици" @@ -176,7 +175,6 @@ "Този Matrix ID не може да бъде намерен, така че поканата може да не бъде получена." "Стаята се напуска" "Пространството се напуска" - "Светъл" "Връзката е копирана в клипборда" "Зарежда се…" "Зарежда се още…" @@ -231,7 +229,6 @@ "%1$d отговора" "В отговор на %1$s" - "Съобщаване за грешка" "Съобщаване за проблем" "Докладът е изпратен" "Редактор на богат текст" @@ -270,7 +267,6 @@ "Успешно" "Предложения" "Синхронизиране" - "Система" "Текст" "Уведомления от трети страни" "Нишка" diff --git a/libraries/ui-strings/src/main/res/values-ca/translations.xml b/libraries/ui-strings/src/main/res/values-ca/translations.xml new file mode 100644 index 0000000000..d8abb7ff73 --- /dev/null +++ b/libraries/ui-strings/src/main/res/values-ca/translations.xml @@ -0,0 +1,387 @@ + + + "Elimina" + + "%1$d dígit introduït" + "%1$d dígits introduïts" + + "Amaga contrasenya" + "Uneix-te a la trucada" + "Vés al final" + "Només mencions" + "Silenciat" + "Noves mencions" + "Pàgina %1$d" + "Pausa" + "Missatge de veu, durada: %1$s, posició actual: %2$s" + "Camp de PIN" + "Reprodueix" + "Votació" + "Votació finalitzada" + "Reacciona amb %1$s" + "Reacciona amb altres emoticones" + "Llegit per %1$s i %2$s" + + "Llegit per %1$s i %2$d més" + "Llegit per %1$s i %2$d més" + + "Llegit per %1$s" + "Toca per mostrar-los tots" + "Elimina la reacció: %1$s" + "Envia fitxers" + "Mostra contrasenya" + "Inicia trucada" + "Menú d\'usuari" + "Mostra els detalls" + "Missatge de veu, durada: %1$s" + "Grava un missatge de veu." + "Atura gravació" + "Accepta" + "Afegeix llegenda" + "Afegeix a la cronologia" + "Tornar" + "Truca" + "Cancel·la" + "Cancel·la per ara" + "Tria una foto" + "Esborra" + "Tanca" + "Completa la verificació" + "Confirma" + "Confirma la contrasenya" + "Continua" + "Copia" + "Copia llegenda" + "Copia l\'enllaç" + "Copia l\'enllaç al missatge" + "Copia text" + "Crea" + "Crea sala" + "Desactiva" + "Desactiva el compte" + "Declina" + "Rebutja i bloqueja" + "Elimina votació" + "Desactiva" + "Descarta" + "Omet" + "Fet" + "Edita" + "Edita llegenda" + "Edita votació" + "Activa" + "Finalitza votació" + "Introdueix PIN" + "Has oblidat la contrasenya?" + "Reenvia" + "Enrere" + "Ignora" + "Convida" + "Convida persones" + "Convida gent a %1$s" + "Convida a la gent a %1$s" + "Invitacions" + "Uneix-te" + "Més informació" + "Surt" + "Surt del xat" + "Surt de la sala" + "Carrega més" + "Gestiona compte" + "Gestiona dispositius" + "Envia missatge" + "Següent" + "No" + "Ara no" + "D\'acord" + "Configuració" + "Obre amb" + "Fixa" + "Resposta ràpida" + "Cita" + "Reacciona" + "Rebutja" + "Elimina" + "Elimina llegenda" + "Elimina missatge" + "Respon" + "Respon al fil" + "Denuncia" + "Informa d\'un error" + "Denuncia contingut" + "Denuncia la conversa" + "Denuncia sala" + "Restableix" + "Restableix identitat" + "Torna-ho a provar" + "Torna a intentar desxifrar" + "Desa" + "Cerca" + "Envia" + "Envia missatge" + "Comparteix" + "Comparteix enllaç" + "Mostra" + "Torna a iniciar sessió" + "Tanca sessió" + "Tanca sessió igualment" + "Omet" + "Comença" + "Inicia un xat" + "Torna a començar" + "Inicia verificació" + "Toca per carregar el mapa" + "Fes una foto" + "Toca per veure opcions" + "Torna-ho a intentar" + "No fixis" + "Mostra a la cronologia" + "Mostra font" + "Sí" + "Sí, torna-ho a intentar" + "El servidor utilizat admet un nou protocol més ràpid. Tanca la sessió i torna-la a iniciar per actualitzar-lo. Si ho fas ara, evitaràs un tancament de sessió forçat quan s\'elimini l\'antic protocol (més endavant)." + "Actualització disponible" + "Sobre l\'aplicació" + "Política d\'ús a acceptar" + "Afegint llegenda" + "Configuració avançada" + "Analítiques" + "Has sortit de la sala" + "Aspecte" + "Àudio" + "Usuaris bloquejats" + "Bombolles" + "Trucada iniciada" + "Còpia de seguretat de xat" + "Copiat al porta-retalls" + "Drets d\'autor" + "Creant sala…" + "Sol·licitud cancel·lada" + "Ha sortit de la sala" + "Invitació rebutjada" + "Error de desxifrat" + "Opcions per a desenvolupadors" + "ID de dispositiu" + "Xat directe" + "No ho tornis a mostrar" + "No s\'ha pogut baixar" + "Baixant" + "(editat)" + "Editant" + "Editant llegenda" + "* %1$s %2$s" + "Fitxer buit" + "Xifrat" + "Xifrat activat" + "Introdueix PIN" + "Error" + "S\'ha produït un error i és possible que no rebis notificacions dels missatges nous. Pots resoldre els problemes de notificacions des de la configuració. + +Motiu: %1$s." + "Tothom" + "Ha fallat" + "Preferit" + "Afegit a preferits" + "Fitxer" + "Fitxer eliminat" + "Fitxer desat" + "Fitxer desat a Descàrregues" + "Reenvia missatge" + "Utilitzats freqüentment" + "GIF" + "Imatge" + "En resposta a %1$s" + "Instal·la APK" + "No s\'ha trobat l\'ID de Matrix, és possible que la invitació no s\'hagi rebut." + "Sortint de la sala" + "Línia copiada al porta-retalls" + "Enllaç copiat al porta-retalls" + "S\'està carregant…" + "Carregant més…" + + "%d més" + "%d més" + + + "%1$d membre" + "%1$d membres" + + "Missatge" + "Accions de missatge" + "Estil dels missatges" + "Missatge eliminat" + "Modern" + "Silencia" + "%1$s (%2$s)" + "Sense resultats" + "Sala sense nom" + "Sense xifrar" + "Fora de línia" + "Llicències de codi obert" + "o" + "Contrasenya" + "Persones" + "Enllaç permanent" + "Permís" + "Fixat" + "Comprova la connexió a Internet" + "Si us plau, espera…" + "Segur que vols finalitzar aquesta votació?" + "Votació: %1$s" + "Vots totals: %1$s" + "Els resultats es mostraran quan hagi finalitzat la votació" + + "%d vot" + "%d vots" + + "Política de privadesa" + "Sala privada" + "Sala pública" + "Reacció" + "Reaccions" + "Motiu" + "Clau de recuperació" + "Actualitzant…" + + "%1$d resposta" + "%1$d respostes" + + "Responent a %1$s" + "Informa d\'un problema" + "S\'ha enviat" + "Editor de text enriquit" + "Sala" + "Nom de sala" + "p. ex. el nom d\'un projecte o grup" + "Canvis desats" + "Desant" + "Bloqueig de pantalla" + "Cerca persones" + "Resultats de la cerca" + "Seguretat" + "Vist per" + "Envia a" + "S\'està enviant…" + "Ha fallat l\'enviament" + "Enviat" + "Servidor no compatible" + "URL del servidor" + "Configuració" + "Ubicació compartida" + "Tancant sessió" + "Alguna cosa ha anat malament" + "S\'ha produït un problema. Torna-ho a intentar." + "Iniciant xat…" + "Adhesiu" + "Correcte" + "Suggeriments" + "Sincronitzant" + "Text" + "Avisos de tercers" + "Fil" + "Tema" + "De què tracta aquesta sala?" + "No s\'ha pogut desxifrar" + "Enviat des d\'un dispositiu insegur" + "No tens accés al missatge" + "La identitat verificada del remitent s\'ha restablert" + "No s\'han pogut enviar invitacions a un o més usuaris." + "No s\'han pogut enviar les invitacions" + "Desbloqueja" + "No silenciïs" + "Trucada no compatible" + "Esdeveniment no compatible" + "Nom d\'usuari" + "Verificació cancel·lada" + "Verificació completada" + "Error de verificació" + "Verificat" + "Verifica dispositiu" + "Verifica identitat" + "Verifica usuari" + "Vídeo" + "Missatge de veu" + "Esperant…" + "Esperant missatge" + "Tu" + "La identitat de %1$s s\'ha restablert. %2$s" + "La identitat de %1$s %2$s s\'ha restablert. %3$s" + "(%1$s)" + "%1$s ha restablert la identitat." + "Omet la verificació" + "L\'enllaç %1$s et portarà a un altre lloc %2$s + +Segur que vols continuar?" + "Revisa aquest enllaç" + "Sala denunciada" + "S\'ha denunciat i abandonat la sala." + "Confirmació" + "Error" + "Correcte" + "Avís" + "Hi ha canvis sense desar." + "Els canvis no s\'han desat. Segur que vols tornar enrere?" + "Desar canvis?" + "Cerca emoticones" + "El servidor utilitzat s\'ha d\'actualitzar per admetre el servei d\'autenticació de Matrix i la creació de comptes." + "No s\'ha pogut crear l\'enllaç permanent" + "%1$s no ha pogut carregar el mapa. Torna-ho a provar més tard." + "No s\'han pogut carregar els missatges" + "%1$s no ha pogut accedir a la teva ubicació. Torna-ho a provar més tard." + "No s\'ha pogut pujar el missatge de veu." + "Missatge no trobat" + "%1$s no té permís per accedir a la teva ubicació. Pots activar l\'accés a Configuració." + "%1$s no té permís per accedir a la teva ubicació. Activa l\'accés a sota." + "%1$s no té permís per accedir al micròfon. Activa\'n l\'accés per poder gravar missatges de veu." + "Això pot ser degut a problemes de xarxa o del servidor." + "Aquesta adreça de sala ja existeix. Prova d\'editar el camp d\'adreça o de canviar el nom de sala" + "Alguns caràcters no estan permesos. Només s\'admeten lletres, dígits i els símbols següents: ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _" + "Alguns missatges no s\'han enviat" + "S\'ha produït un error" + "🔐️ Uneix-te a %1$s" + "Ei, xateja amb mi a %1$s: %2$s" + "%1$s Android" + "Sacseja per informar d\'errors" + "No s\'ha pogut seleccionar el contingut. Torna-ho a provar." + "Prem un missatge i selecciona “%1$s“ per incloure\'l aquí." + "Fixa els missatges importants perquè es puguin trobar fàcilment" + + "%1$d missatge fixat" + "%1$d missatges fixats" + + "Missatges fixats" + "No pots confirmar-la? Ves al teu compte per restablir la identitat." + "Omet la verificació i envia" + "Pots ometre la verificació i enviar el missatge igualment o pots cancel·lar i tornar-ho a intentar més tard, quan s\'hagi tornat a verificar %1$s." + "El teu missatge no s\'ha enviat perquè la identitat verificada de %1$s s\'ha reiniciat" + "Envia el missatge igualment" + "%1$s està utilitzant un o més dispositius no verificats. Pots enviar el missatge igualment o pots cancel·lar-lo i tornar-ho a provar més tard quan %2$s hagi verificat tots els seus dispositius." + "El teu missatge no s\'ha enviat perquè %1$s no ha verificat tots els dispositius" + "Un o més dels teus dispositius no estan verificats. Pots enviar el missatge igualment o cancel·lar-lo i tornar-ho a intentar més tard després d\'haver verificat tots els dispositius." + "El missatge no s\'ha enviat perquè no has verificat un o més dels teus dispositius." + "No s\'ha pogut processar el contingut que s\'havia de pujar. Torna-ho a provar." + "No s\'han pogut obtenir els detalls d\'usuari" + "Missatge a %1$s" + "%1$s de %2$s" + "%1$s missatges fixats" + "Crregant missatge…" + "Veure-ho tot" + "Xat" + "Comparteix ubicació" + "Comparteix la meva ubicació" + "Obre a Apple Maps" + "Obre a Google Maps" + "Obre a OpenStreetMap" + "Comparteix aquesta ubicació" + "Missatge no enviat perquè %1$s ha restablert la seva identitat verificada." + "Missatge no enviat perquè %1$s no ha verificat tots els dispositius." + "Missatge no enviat perquè no has verificat un o més dels teus dispositius." + "Ubicació" + "Versió: %1$s (%2$s)" + "en" + "L\'històric de missatges no està disponible en aquest dispositiu" + "Has de verificar aquest dispositiu per accedir a l\'històric de missatges" + "No tens accés al missatge" + "No s\'ha pogut desxifrar el missatge" + "Aquest missatge s\'ha bloquejat perquè no has verificat el teu dispositiu o perquè el remitent a de verificar la teva identitat." + 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 4343f2980b..5f45bea93b 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -15,6 +15,7 @@ "Podrobnosti o šifrování" "Rozbalit textové pole zprávy" "Skrýt heslo" + "Informace" "Připojit se k hovoru" "Přejít dolů" "Přesunout mapu na mou polohu" @@ -49,11 +50,15 @@ "Avatar místnosti" "Odeslat soubory" "Poloha odesílatele" + "Odesláno %1$s v %2$s" "Vyžaduje se časově omezená akce, na ověření máte jednu minutu" + "Nastavení, vyžaduje akci" "Zobrazit heslo" "Zahájit hovor" "Zahájit videohovor" "Zahájit hlasový hovor" + "Vlákno v %1$s" + "Vlákna v %1$s" "Místnost s náhrobkem" "Avatar uživatele" "Uživatelské menu" @@ -71,6 +76,7 @@ "Hovor" "Zrušit" "Prozatím zrušit" + "Vyberte soubor" "Vybrat fotku" "Vymazat" "Zavřít" @@ -198,7 +204,6 @@ "Pokročilá nastavení" "obrázek" "Analytika" - "Synchronizace oznámení…" "Opustili jste místnost" "Byli jste odhlášeni z relace" "Vzhled" @@ -206,7 +211,9 @@ "Beta" "Blokovaní uživatelé" "Bubliny" + "Hovor odmítnut" "Hovor zahájen" + "Odmítli jste hovor" "Záloha chatu" "Zkopírováno do schránky" "Autorská práva" @@ -216,7 +223,6 @@ "Místnost opuštěna" "Opustit prostor" "Pozvánka odmítnuta" - "Tmavé" "Chyba dešifrování" "Popis" "Možnosti pro vývojáře" @@ -242,6 +248,7 @@ Důvod: %1$s." "Selhalo" "Oblíbené" "Oblíbené" + "Synchronizace oznámení…" "Soubor" "Soubor smazán" "Soubor uložen" @@ -255,7 +262,6 @@ Důvod: %1$s."
"Tento Matrix identifikátor nelze najít, takže pozvánka nemusí být přijata." "Opuštění místnosti" "Opuštění prostoru" - "Světlý" "Řádek zkopírován do schránky" "Odkaz zkopírován do schránky" "Připojit nové zařízení" @@ -324,7 +330,6 @@ Důvod: %1$s."
"%1$d odpovědí" "Odpověď na %1$s" - "Nahlásit chybu" "Nahlásit problém" "Zpráva odeslána" "Editor formátovaného textu" @@ -381,7 +386,6 @@ Důvod: %1$s."
"Doporučeno" "Návrhy" "Synchronizace" - "Systém" "Text" "Oznámení třetích stran" "Vlákno" @@ -470,6 +474,9 @@ Opravdu chcete pokračovat?"
"Omlouváme se, došlo k chybě" "🔐️ Připojte se ke mně na %1$s" "Ahoj, ozvi se mi na %1$s: %2$s" + "Sdílení polohy v reálném čase" + "Probíhá sdílení polohy" + "%1$s Aktuální poloha" "%1$s Android" "Zatřeste zařízením pro nahlášení chyby" "Snímek obrazovky" diff --git a/libraries/ui-strings/src/main/res/values-cy/translations.xml b/libraries/ui-strings/src/main/res/values-cy/translations.xml index 9c4b04b9d5..00e2a39297 100644 --- a/libraries/ui-strings/src/main/res/values-cy/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cy/translations.xml @@ -198,7 +198,6 @@ "Wedi gadael yr ystafell" "Gofod chwith" "Wedi gwrthod y gwahoddiad" - "Tywyll" "Gwall dadgryptio" "Disgrifiad" "Dewisiadau datblygwr" @@ -235,7 +234,6 @@ Rheswm: %1$s."
"Gosod APK" "Nid oes modd dod o hyd i\'r ID Matrics hwn, felly mae\'n bosibl na fydd y gwahoddiad yn cael ei dderbyn." "Gadael ystafell" - "Golau" "Llinell wedi\'i chopïo i\'r clipfwrdd" "Dolen wedi\'i chopïo i\'r clipfwrdd" "Yn Llwytho…" @@ -309,7 +307,6 @@ Rheswm: %1$s."
"%1$d ateb" "Yn ymateb i %1$s" - "Adrodd ar wall" "Adrodd am broblem" "Adroddiad wedi ei gyflwyno" "Golygydd testun cyfoethog" @@ -360,7 +357,6 @@ Rheswm: %1$s."
"Llwyddiant" "Awgrymiadau" "Cydweddu" - "System" "Testun" "Hysbysiadau trydydd parti" "Edefyn" 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 e9ed8edaa4..ce79b8972b 100644 --- a/libraries/ui-strings/src/main/res/values-da/translations.xml +++ b/libraries/ui-strings/src/main/res/values-da/translations.xml @@ -14,9 +14,10 @@ "Krypteringsoplysninger" "Udvid tekstfeltet for beskeder" "Skjul adgangskode" + "Info" "Deltag i opkald" "Hop til bunden" - "Flyt kortet til min lokation" + "Flyt kortet til min placering" "Kun omtaler" "Lyd slået fra" "Nye omtaler" @@ -48,8 +49,10 @@ "Send filer" "Afsenderens placering" "Tidsbegrænset handling påkrævet, du har et minut til at bekræfte" + "Indstillinger, handling påkrævet" "Vis adgangskode" "Start et opkald" + "Start et videoopkald" "Start et taleopkald" "Deaktiveret rum" "Avatar for bruger" @@ -87,12 +90,15 @@ "Deaktiver konto" "Afvis" "Afvis og blokér" + "Slet" + "Slet konto" "Slet afstemning" "Fravælg alle" "Deaktiver" "Kassér" "Afvis" "Færdig" + "Hent" "Redigér" "Rediger billedtekst" "Redigér afstemning" @@ -160,7 +166,7 @@ "Send talebesked" "Del" "Del link" - "Del liveplacering" + "Del placering live" "Vis" "Log ind igen" "Fjern denne enhed" @@ -192,7 +198,6 @@ "Avancerede indstillinger" "et billede" "Analyse-værktøj" - "Synkroniserer notifikationer…" "Du forlod rummet" "Du blev logget ud af sessionen" "Udseende" @@ -200,7 +205,9 @@ "Beta" "Blokerede brugere" "Bobler" + "Opkald afvist" "Opkald startet" + "Du afviste et opkald" "Backup af samtale" "Kopieret til udklipsholder" "Ophavsret" @@ -210,7 +217,6 @@ "Forlod rummet" "Forlod klynge" "Invitationen blev afvist" - "Mørkt tema" "Fejl under dekryptering" "Beskrivelse" "Indstillinger for udviklere" @@ -236,6 +242,7 @@ "Mislykkedes" "Favorit" "Favoritmarkeret" + "Synkroniserer notifikationer…" "Fil" "Fil slettet" "Fil gemt" @@ -249,7 +256,6 @@ "Dette Matrix-ID kan ikke findes, så invitationen modtages muligvis ikke." "Forlader rummet" "Forlader klynge" - "Lyst tema" "Linje kopieret til udklipsholder" "Linket er kopieret til udklipsholderen" "Forbind ny enhed" @@ -315,7 +321,6 @@ "%1$d svar" "Svarer til %1$s" - "Rapportér en fejl" "Anmeld et problem" "Anmeldelsen er indsendt" "Rich text editor" @@ -357,7 +362,7 @@ "Noget gik galt" "Vi stødte på et problem. Prøv venligst igen." "Klynge" - "Medlemmer af rummet" + "Medlemmer af klyngen" "Hvad handler denne klynge om?" "%1$d Klynge" @@ -369,7 +374,6 @@ "Forslag" "Forslag" "Synkroniserer" - "System" "Tekst" "Tredjepartsmeddelelser" "Tråd" @@ -465,6 +469,13 @@ Er du sikker på, at du vil fortsætte?"
"Valgmuligheder" "Fjern %1$s" "Indstillinger" + "Ingen deler deres placering" + "Deler placering live" + + "%1$d person" + "%1$d personer" + + "På kortet" "Det lykkedes ikke at vælge medie. Prøv igen." "Tryk på en besked og vælg \"%1$s\" for at inkludere den her." "Fastgør vigtige beskeder, så de let kan opdages" @@ -489,14 +500,15 @@ Er du sikker på, at du vil fortsætte?"
"Besked i %1$s" "Udvid" "Reducér" + "Deler placering live" "Du ser allerede dette rum!" "%1$s af %2$s" "%1$s Fastgjorte beskeder" "Indlæser besked…" "Se alle" "Samtale" - "Del lokation" - "Del min lokation" + "Del placering" + "Del min placering" "Åbn i Apple Maps" "Åbn i Google Maps" "Åbn i OpenStreetMap" @@ -512,7 +524,7 @@ Er du sikker på, at du vil fortsætte?" "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" + "Placering" "Version: %1$s (%2$s)" "da" "Historiske beskeder er ikke tilgængelige på denne enhed" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 81cb12eb78..5b7f52cf4e 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -192,7 +192,6 @@ "Erweiterte Einstellungen" "ein Bild" "Analysedaten" - "Benachrichtigungen werden synchronisiert…" "Du hast den Chat verlassen" "Du wurdest aus der Sitzung abgemeldet." "Erscheinungsbild" @@ -210,7 +209,6 @@ "Hat den Chat verlassen" "Space verlassen" "Einladung abgelehnt" - "Dunkel" "Dekodierungsfehler" "Beschreibung" "Entwickleroptionen" @@ -236,6 +234,7 @@ Grund: %1$s." "Fehlgeschlagen" "Favorit" "Favorisiert" + "Benachrichtigungen werden synchronisiert…" "Datei" "Datei wurde gelöscht" "Datei gespeichert" @@ -249,7 +248,6 @@ Grund: %1$s." "Diese Matrix Kennung wurde nicht gefunden, daher wird die Einladung möglicherweise nicht empfangen." "Chat verlassen" "Space wird verlassen" - "Hell" "Zeile in die Zwischenablage kopiert" "Link in die Zwischenablage kopiert" "Neues Gerät verknüpfen" @@ -316,7 +314,6 @@ Grund: %1$s." "%1$d Antworten" "%1$s antworten" - "Einen Fehler melden" "Ein Problem melden" "Bericht eingereicht" "Rich-Text-Editor" @@ -370,7 +367,6 @@ Grund: %1$s." "Empfohlen" "Vorschläge" "Synchronisieren" - "System" "Text" "Hinweise von Drittanbietern" "Thread" @@ -447,6 +443,7 @@ Möchtest du wirklich fortfahren?" "%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut." "Fehler beim Hochladen der Sprachnachricht." "Der Chat existiert nicht mehr oder die Einladung ist nicht mehr gültig." + "Bitte aktiviere dein GPS, um auf standortbezogene Funktionen zugreifen zu können." "Nachricht nicht gefunden" "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren." "%1$s hat keine Berechtigung, auf deinen Standort zuzugreifen. Erlaube unten den Zugriff." @@ -473,7 +470,7 @@ Möchtest du wirklich fortfahren?" "%1$d fixierte Nachrichten" "Fixierte Nachrichten" - "Du wirst jetzt zu deinem %1$s Konto geleitet, um deine Identität zurückzusetzen. Danach wirst du zur App zurückgebracht." + "Du wirst gleich zu deinem %1$s Konto weitergeleitet, um deine digitale Identität zurückzusetzen. Danach kehrst du zur App zurück." "Bestätigung nicht möglich? Rufe dein Konto auf, um deine digitale Identität zurückzusetzen." "Verifizierung zurückziehen und senden" "Du kannst deine Verifizierung zurückziehen und diese Nachricht trotzdem senden, oder du kannst vorerst abbrechen und es später noch einmal versuchen, nachdem du %1$s erneut verifiziert hast." @@ -509,7 +506,7 @@ Möchtest du wirklich fortfahren?" "Spaces" "Geteilt %1$s" "Auf der Karte" - "Nachricht nicht gesendet, weil sich die verifizierte Identität von %1$s geändert hat." + "Die Nachricht wurde nicht gesendet, da die verifizierte digitale Identität von %1$s zurückgesetzt wurde." "Die Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat." "Die Nachricht wurde nicht gesendet, weil du eines oder mehrere deiner Geräte nicht verifiziert hast." "Standort" @@ -519,5 +516,5 @@ Möchtest du wirklich fortfahren?" "Für den Zugriff auf den Nachrichtenverlauf musst du dieses Gerät verifizieren" "Du hast keinen Zugriff auf diese Nachricht." "Nachricht kann nicht entschlüsselt werden" - "Diese Nachricht wurde entweder blockiert, weil du dein Gerät nicht verifiziert hast oder weil der Absender deine Identität verifizieren muss." + "Diese Nachricht wurde entweder blockiert, weil du dein Gerät nicht verifiziert hast, oder weil der Absender deine digitale Identität verifizieren muss." diff --git a/libraries/ui-strings/src/main/res/values-el/translations.xml b/libraries/ui-strings/src/main/res/values-el/translations.xml index beb97c54d1..a04e7aceb6 100644 --- a/libraries/ui-strings/src/main/res/values-el/translations.xml +++ b/libraries/ui-strings/src/main/res/values-el/translations.xml @@ -50,6 +50,7 @@ "Απαιτείται ενέργεια περιορισμένης χρονικής διάρκειας, έχετε ένα λεπτό για επαλήθευση." "Εμφάνιση κωδικού πρόσβασης" "Ξεκίνησε μια κλήση" + "Ξεκινήστε μια βιντεοκλήση" "Έναρξη φωνητικής κλήσης" "Θαμένη αίθουσα" "Άβαταρ χρήστη" @@ -120,6 +121,7 @@ "Αποχώρηση από τον χώρο" "Φόρτωσε περισσότερα" "Διαχείριση λογαριασμού" + "Διαχείριση λογαριασμού και συσκευών" "Διαχείριση συσκευών" "Διαχείριση αίθουσών" "Στείλε" @@ -170,6 +172,7 @@ "Ξανά από την αρχή" "Έναρξη επαλήθευσης" "Πάτα για φόρτωση χάρτη" + "Διακοπή" "Τράβηξε φωτογραφία" "Πάτα για επιλογές" "Μετάφραση" @@ -190,7 +193,6 @@ "Ρυθμίσεις για προχωρημένους" "μια εικόνα" "Στατιστικά στοιχεία" - "Συγχρονισμός ειδοποιήσεων…" "Αποχωρήσατε από την αίθουσα" "Αποσυνδεθήκατε από την περίοδο λειτουργίας" "Εμφάνιση" @@ -208,7 +210,6 @@ "Αποχώρησε από την αίθουσα" "Αποχωρήσατε από τον χώρο" "Η πρόσκληση απορρίφθηκε" - "Σκοτεινό" "Σφάλμα αποκρυπτογράφησης" "Περιγραφή" "Επιλογές προγραμματιστή" @@ -224,6 +225,7 @@ "Κενό αρχείο" "Κρυπτογράφηση" "Η κρυπτογράφηση ενεργοποιήθηκε" + "Τελειώνει στις %1$s" "Εισήγαγε το PIN σου" "Σφάλμα" "Παρουσιάστηκε σφάλμα, ενδέχεται να μην λαμβάνεις ειδοποιήσεις για νέα μηνύματα. Αντιμετώπισε το πρόβλημα με τις ειδοποιήσεις από τις ρυθμίσεις. @@ -233,6 +235,7 @@ "Απέτυχε" "Αγαπημένο" "Είναι αγαπημένο" + "Συγχρονισμός ειδοποιήσεων…" "Αρχείο" "Το αρχείο διαγράφηκε" "Το αρχείο αποθηκεύτηκε" @@ -246,10 +249,11 @@ "Αυτό το Matrix ID δεν μπορεί να βρεθεί, επομένως η πρόσκληση ενδέχεται να μην ληφθεί." "Αποχώρηση από την αίθουσα" "Αποχωρείτε από τον χώρο" - "Φωτεινό" "Η γραμμή αντιγράφηκε στο πρόχειρο" "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο" "Σύνδεση νέας συσκευής" + "Ζωντανή τοποθεσία" + "Η ζωντανή τοποθεσία έληξε" "Φόρτωση…" "Φόρτωση περισσότερων…" @@ -311,7 +315,6 @@ "%1$d απαντήσεις" "Απάντηση σε %1$s" - "Αναφορά σφάλματος" "Αναφορά προβλήματος" "Η αναφορά υποβλήθηκε" "Επεξεργαστής εμπλουτισμένου κειμένου" @@ -346,6 +349,7 @@ "Ρυθμίσεις" "Κοινή χρήση χώρου" "Τα νέα μέλη βλέπουν το ιστορικό" + "Κοινόχρηστη ζωντανή τοποθεσία" "Κοινόχρηστη τοποθεσία" "Κοινόχρηστος χώρος" "Αφαίρεση συσκευής" @@ -364,10 +368,10 @@ "Προτεινόμενο" "Προτάσεις" "Συγχρονισμός" - "Σύστημα" "Κείμενο" "Ειδοποιήσεις τρίτων" "Νήμα" + "Νήματα" "Θέμα" "Τι αφορά αυτή η αίθουσα;" "Δεν είναι δυνατή η αποκρυπτογράφηση" @@ -398,6 +402,7 @@ "Φωνητικό μήνυμα" "Αναμονή…" "Αναμονή για αυτό το μήνυμα" + "Αναμονή για ζωντανή τοποθεσία…" "Οποιοσδήποτε μπορεί να δει το ιστορικό" "Εσύ" "%1$s (%2$s) μοιράστηκε αυτό το μήνυμα, καθώς δεν ήσασταν στην αίθουσα όταν στάλθηκε." diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index fa0bc700cb..b03223a70e 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -165,7 +165,6 @@ "Solicitud cancelada" "Saliste de la sala" "Invitación rechazada" - "Oscuro" "Error de descifrado" "Opciones de desarrollador" "ID de dispositivo" @@ -201,7 +200,6 @@ Motivo: %1$s." "Instalar APK" "No se encontró este ID de Matrix, por lo que es posible que no se reciba la invitación." "Saliendo de la sala" - "Claro" "Línea copiada al portapapeles" "Enlace copiado al portapapeles" "Cargando…" @@ -251,7 +249,6 @@ Motivo: %1$s." "Clave de recuperación" "Recargando…" "Respondiendo a %1$s" - "Informar de un error" "Informar de un problema" "Informe enviado" "Editor de texto enriquecido" @@ -281,7 +278,6 @@ Motivo: %1$s." "Terminado" "Sugerencias" "Sincronizando" - "Sistema" "Texto" "Avisos de terceros" "Hilo" diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index beedf175bd..e236e2e3f4 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -1,5 +1,6 @@ + "Lisa tunnuspilt" "Reageeri: %1$s" "Aadress" "Tunnuspilt" @@ -9,13 +10,16 @@ "%1$d number sisestatud" "%1$d numbrit sisestatud" + "Kestus: %1$s" "Muuda tunnuspilti" "Täisaadress saab olema %1$s" "Krüptimise üksikasjad" "Laienda tekstivälja" "Peida salasõna" + "Teave" "Liitu kõnega" "Mine lõppu" + "Mine lugemata sõnumite juurde" "Nihuta kaart minu asukohta" "Ainult mainimised" "Summutatud" @@ -32,6 +36,7 @@ "Taasesituse kiirus" "Küsitlus" "Lõppenud küsitlus" + "Asukoht: %1$s" "QR-kood" "Reageeri emotikoniga %1$s" "Reageeri mõne muu emotikoniga" @@ -47,11 +52,15 @@ "Jututoa tunnuspilt" "Saada faile" "Saatja asukoht" + "%1$s saatis selle %2$s" "Palun tee see ajapiiranguga toiming, sul on aega üks minut" + "Seaded, vajalik on tegevus" "Näita salasõna" "Helista" "Alusta videokõnet" "Helista" + "Jutulõng „%1$s“ jututoas" + "Jutulõngad „%1$s“ jututoas" "Lõpetatuks märgitud jututuba" "Kasutaja tunnuspilt" "Kasutajamenüü" @@ -69,6 +78,7 @@ "Helista" "Loobu" "Hetkel jäta tegemata" + "Vali fail" "Vali foto" "Selge" "Sulge" @@ -88,12 +98,16 @@ "Eemalda konto kasutusest" "Keeldu" "Keeldu ja blokeeri" + "Kustuta" + "Kustuta kasutajakonto" + "Kustuta fail" "Kustuta küsitlus" "Eemalda kõik valikud" "Lülita välja" "Loobu" "Lõpeta" "Valmis" + "Laadi alla" "Muuda" "Muuda selgitust" "Muuda küsitlust" @@ -193,7 +207,6 @@ "Täiendavad seadistused" "pilt" "Analüütika" - "Sünkroonin teavitusi…" "Sina lahkusid jututoast" "Sa olid sessioonist väljaloginud" "Välimus" @@ -201,7 +214,10 @@ "Beetaversioon" "Blokeeritud kasutajad" "Mullid" + "Osapool keeldus kõnest" + "Kõne on pooleli" "Kõne algas" + "Sa keeldusid kõnest" "Vestluse varukoopia" "Kopeeritud lõikelauale" "Autoriõigused" @@ -211,7 +227,6 @@ "Lahkus jututoast" "Lahkus kogukonnast" "Keeldusid kutsest" - "Tume" "Dekrüptimisviga" "Kirjeldus" "Arendaja valikud" @@ -227,6 +242,7 @@ "Tühi fail" "Krüptimine" "Krüptimine on kasutusel" + "Lõpeb kell %1$s" "Sisesta oma PIN-kood" "Viga" "Tekkis viga ja sa ei pruugi enam saada uute sõnumite kohta teavitusi. Palun kontrolli teavituste seadistusi ja proovi viga tuvastada. @@ -236,6 +252,7 @@ Põhjus: %1$s." "Ei õnnestunud" "Lemmik" "Märgitud lemmikuks" + "Sünkroonin teavitusi…" "Fail" "Fail on kustutatud" "Fail on salvestatud" @@ -243,16 +260,18 @@ Põhjus: %1$s." "Edasta sõnum" "Sagedasti kasutatud" "GIF" + "Rühmakõne on pooleli" "Pilt" "Vastuseks kasutajale %1$s" "Paigalda APK-failist" "Sellist Matrix\'i kasutajatunnust ei õnnestu leida, seega sõnumit ilmselt keegi kätte ei saa." "Oled lahkumas jututoast" "Oled lahkumas kogukonnast" - "Hele" "Rida on kopeeritud lõikelauale" "Link on kopeeritud lõikelauale" "Seo uus seade" + "Asukoha jagamine reaalajas" + "Reaalajas asukoha jagamine on lõppenud" "Laadime…" "Laadime veel…" @@ -279,6 +298,7 @@ Põhjus: %1$s." "Võrgust väljas" "Avatud lähtekoodiga litsentsid" "või" + "Muud valikud" "Salasõna" "Inimesed" "Püsilink" @@ -288,6 +308,7 @@ Põhjus: %1$s." "Palun oota…" "Kas oled kindel, et soovid selle küsitluse lõpetada?" "Küsitlus: %1$s" + "Küsitlus" "Hääli kokku: %1$s" "Tulemused on näha peale küsitluse lõppemist" @@ -313,7 +334,6 @@ Põhjus: %1$s." "%1$d vastust" "Vastates kasutajale %1$s" - "Teata veast" "Teata veast" "Veateade on saadetud" "Vormindatud teksti toimeti" @@ -348,9 +368,10 @@ Põhjus: %1$s." "Seadistused" "Jaga kogukonda" "Uued liikmed näevad ajalugu" + "Reaalajas jagatud asukoht" "Jagatud asukoht" "Jagatud kogukond" - "Logime välja" + "Seade on eemaldamisel" "Midagi läks valesti" "Tekkis viga. Palun proovi uuesti." "Kogukond" @@ -366,7 +387,6 @@ Põhjus: %1$s." "Soovitatud" "Soovitused" "Sünkroniseerime" - "Süsteem" "Tekst" "Kolmandate osapoolte teatised" "Jutulõng" @@ -376,13 +396,14 @@ Põhjus: %1$s." "Dekrüptimine ei olnud võimalik" "Saadetud ebaturvalisest seadmest" "Sul pole ligipääsu antud sõnumile" - "Saatja verifitseeritud identiteet on lähtestatud" + "Saatja digitaalse identiteet on lähtestatud" "Kutset polnud võimalik saata ühele või enamale kasutajale." "Kutse(te) saatmine ei õnnestunud" "Eemalda lukustus" "Lõpeta summutamine" "See kõne pole toetatud" "Toetamata sündmus" + "%1$s helistas" "Kasutajanimi" "Verifitseerimine on katkestatud" "Verifitseerimine on tehtud" @@ -401,17 +422,19 @@ Põhjus: %1$s." "Häälsõnum" "Ootame…" "Ootame selle sõnumi dekrüptimisvõtit" + "Ootan asukoha jagamist reaalajas…" "Kõik võivad ajalugu näha" "Sina" "Kuna sind polnud saatmise ajal jututoas, siis %1$s (%2$s) jagas seda sõnumit sinuga." "%1$s jagas seda sõnumit, kuna sind ei olnud selle algse saatmise ajal jututoas." "See jututuba on seadistatud sedaviisi, et ka uued liikmed saavad lugeda varasemat ajalugu. %1$s" - "Kasutaja %1$s võrguidentiteet on lähtestatud. %2$s" - "Kasutaja %1$s %2$s võrguidentiteet on lähtestatud. %3$s" + "Kasutaja %1$s digitaalse identiteet on lähtestatud. %2$s" + "Kasutaja %1$s %2$s digitaalse identiteet on lähtestatud. %3$s" "(%1$s)" - "%1$s kasutaja verifitseeritud identiteet on lähtestatud." - "%1$s kasutaja (%2$s kasutajanimi) verifitseeritud identiteet on lähtestatud. %3$s" + "%1$s kasutaja digitaalse identiteet on lähtestatud." + "%1$s kasutaja (ksutajanimega %2$s) digitaalse identiteet on lähtestatud. %3$s" "Võta verifitseerimine tagasi" + "Luba juurdepääs" "%1$s link viib sind teise veebisaiti %2$s Kas sa oled kindel, et soovid jätkata?" @@ -441,6 +464,7 @@ Kas sa oled kindel, et soovid jätkata?" "Rakendus %1$s ei suutnud tuvastada sinu asukohta. Palun proovi hiljem uuesti." "Sinu häälsõnumi üleslaadimine ei õnnestunud." "Seda jututuba pole enam olemas või pole see kutse enam kehtiv." + "Asukohapõhise funktsionaalsuse kasutamiseks palun lülita nutiseadmes GPS sisse." "Sõnumit ei leidu" "Rakendusel %1$s puudub õigus sinu asukohta tuvastada. Sa saad seda lubada süsteemi seadistustest." "Rakendusel %1$s puudub õigus sinu asukohta tuvastada. Järgnevalt anna vastavad õigused." @@ -452,6 +476,9 @@ Kas sa oled kindel, et soovid jätkata?" "Vabandust, ilmnes viga" "🔐️ Liitu minuga rakenduses %1$s" "Hei, suhtle minuga %1$s võrgus: %2$s" + "Asukoha jagamine reaalajas" + "Asukoha jagamine käimas" + "Asukoht reaalajas: %1$s" "%1$s Android" "Veast teatamiseks raputa nutiseadet ägedalt" "Ekraanitõmmis" @@ -461,6 +488,11 @@ Kas sa oled kindel, et soovid jätkata?" "Seadistused" "Mitte keegi ei jaga oma asukohta" "Asukoht on jagamisel reaalajas" + + "%1$d isik" + "%1$d isikut" + + "Kaardil" "Meediafaili valimine ei õnnestunud. Palun proovi uuesti." "Siia lisamiseks vajuta sõnumil ja vali „%1$s“." "Et olulisi sõnumeid oleks lihtsam leida, tõsta nad esile" @@ -469,11 +501,11 @@ Kas sa oled kindel, et soovid jätkata?" "%1$d esiletõstetud sõnumit" "Esiletõstetud sõnumid" - "Oma võrguidentiteedi lähtestamiseks suuname sind %1$s kasutajakonto halduse lehele. Hiljem suunatakse sind tagasi sama rakenduse juurde." - "Sa ei saa seda kinnitada? Ava oma kasutajakonto haldus ja lähtesta oma võrguidentiteet." + "Oma digitaalse identiteedi lähtestamiseks suuname sind %1$s kasutajakonto halduse lehele. Hiljem suunatakse sind tagasi sama rakenduse juurde." + "Sa ei saa seda kinnitada? Ava oma kasutajakonto haldus ja lähtesta oma digitaalne identiteet." "Unusta verifitseerimine ja saada ikkagi" "Sa võid jätta verifitseerimisvea tähelepanuta ja sõnumi ikkagi saata või katkestad saatmise ja peale kasutaja %1$s verifitseerimist proovid seda uuesti." - "Sinu sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud." + "Sinu sõnum on saatmata, kuna kasutaja %1$s digitaalse identiteet on lähtestatud." "Saada sõnum ikkagi" "%1$s kasutab ühte või enamat verifitseerimata seadet. Sa võid sõnumi ikkagi saata või katkestad selle ning ootad kuni %2$s on kõik oma seadmed verifitseerinud ning proovid seejärel uuesti." "Sinu sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid" @@ -485,6 +517,7 @@ Kas sa oled kindel, et soovid jätkata?" "Sõnum jututoas %1$s" "Näita rohkem" "Näita vähem" + "Asukoht on jagamisel reaalajas" "Sa juba vaatad seda jututuba!" "%1$s / %2$s" "%1$s esiletõstetud sõnumit" @@ -496,14 +529,20 @@ Kas sa oled kindel, et soovid jätkata?" "Ava Apple Mapsis" "Ava Google Mapsis" "Ava OpenStreetMapis" - "Jaga seda asukohta" + "Jaga valitud asukohta" "Jagamise valikud" "Sinu loodud kogukonnad ning need, millega oled liitunud." "%1$s • %2$s" "Jututubade haldamiseks võid luua kogukondi" "Kogukond: %1$s" "Kogukonnad" - "Sõnum on saatmata, kuna kasutaja %1$s verifitseeritud identiteet on lähtestatud." + "Jagatud %1$s" + "Kaardil" + + "%1$d liitus" + "%1$d liitusid" + + "Sõnum on saatmata, kuna kasutaja %1$s digitaalse identiteet on lähtestatud." "Sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid." "Kuna sa pole üks või enamgi oma seadet verifitseerinud, siis sinu sõnum on saatmata." "Asukoht" @@ -513,5 +552,7 @@ Kas sa oled kindel, et soovid jätkata?" "Ligipääsuks vanadele sõnumitele pead selle seadme verifitseerima" "Sul pole ligipääsu antud sõnumile" "Sõnumi dekrüptimine ei õnnestu" - "Kuna seade on verifitseerimata või saatja pole sind verifitseerinud, siis sõnumi näitamine on blokeeritud." + "Kuna seade on verifitseerimata või saatja pole sinu digitaalset identiteeti verifitseerinud, siis sõnumi näitamine on blokeeritud." + "Sinu nutiseadme tarkvara on liiga vana, vajalik on Android 8 või uuem." + "Kaartide joonistamine pole toetatud" diff --git a/libraries/ui-strings/src/main/res/values-eu/translations.xml b/libraries/ui-strings/src/main/res/values-eu/translations.xml index bef5f1a4b5..0c046b8bb4 100644 --- a/libraries/ui-strings/src/main/res/values-eu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-eu/translations.xml @@ -169,7 +169,6 @@ "Eskaera bertan behera utzi da" "Gelatik atera da" "Gonbidapenari uko egin zaio" - "Iluna" "Deszifratze-errorea" "Garapen aukerak" "Gailuaren IDa" @@ -204,7 +203,6 @@ Arrazoia: %1$s." "Instalatu APK" "Matrix IDa ezin da topatu eta, beraz, litekeena da gonbidapena ez jasotzea." "Gelatik ateratzen" - "Argia" "Lerroa arbelean kopiatu da" "Esteka arbelean kopiatu da" "Kargatzen…" @@ -256,7 +254,6 @@ Arrazoia: %1$s." "Berreskuratze-gakoa" "Freskatzen…" "%1$s(r)i erantzuten" - "Eman akats baten berri" "Eman arazo baten berri" "Salaketa bidali da" "Testu aberatsaren editorea" @@ -288,7 +285,6 @@ Arrazoia: %1$s." "Arrakasta" "Iradokizunak" "Sinkronizatzen" - "Sistema" "Testua" "Hirugarrenei buruzko oharrak" "Haria" diff --git a/libraries/ui-strings/src/main/res/values-fa/translations.xml b/libraries/ui-strings/src/main/res/values-fa/translations.xml index fbe716bc49..c14f281f0d 100644 --- a/libraries/ui-strings/src/main/res/values-fa/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml @@ -145,8 +145,8 @@ "هم‌رسانی پیوند" "نمایش" "ورود دوباره" - "خروج" - "خروج به هر صورت" + "برداشتن این افزاره" + "به هر حال این دستگاه را حذف کنید" "پرش" "آغاز" "آغاز گپ" @@ -185,7 +185,6 @@ "اتاق را ترک کرد" "فضا را ترک کرد" "دعوت لغو شد" - "تیره" "خطای رمزگشایی" "توضیح" "گزینه‌های توسعه دهنده" @@ -220,11 +219,14 @@ "شناسهٔ ماتریکس نتوانست پیدا شود. ممکن است دعوت نرسیده باشد." "ترک کردن اتاق" "ترک کردن فضا" - "روشن" "خط در تخته‌گیره رونوشت شد" "پیوند در تخته‌گیره رونوشت شد" "بار کردن…" "بار کردن بیش‌تر…" + + "%d دیگر" + "%d دیگران" + "%1$d عضو" "%1$d عضو" @@ -270,7 +272,6 @@ "کلید بازیابی" "تازه سازی…" "پاسخ دادن به %1$s" - "گزارش یک اشکال" "گزارش مشکل" "گزارش ثبت شد" "ویرایشگر متن غنی" @@ -305,7 +306,6 @@ "موفّقیت" "پیشنهادها" "هم‌گام ساختن" - "سامانه" "متن" "تذکّرهای سوم‌شخص" "رشته" @@ -389,7 +389,7 @@ "گشودن در نقشه‌های اپل" "گشودن در نقشه‌های گوگل" "گشودن در اوپن‌استریت‌مپ" - "هم‌رسانی این مکان" + "هم‌رسانی مکان گزیده" "فضاهایی که ساخته یا پیوسته‌اید." "%1$s • %2$s" "‏%1$s فضا" 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 48601352ba..096652fbd4 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -9,13 +9,16 @@ "%1$d numero syötetty" "%1$d numeroa syötetty" + "Kesto: %1$s" "Muokkaa avataria" "Täysi osoite tulee olemaan %1$s" "Salauksen tiedot" "Laajenna viestin tekstikenttä" "Piilota salasana" + "Tiedot" "Liity puheluun" "Siirry loppuun" + "Siirry lukemattomiin" "Siirrä kartta sijaintiini" "Vain maininnat" "Mykistetty" @@ -32,6 +35,7 @@ "Toistonopeus" "Kysely" "Päättynyt kysely" + "Sijainti: %1$s" "QR-koodi" "Lisää reaktio: %1$s" "Reagoi muilla emojeilla" @@ -47,11 +51,15 @@ "Huoneen avatar" "Lähetä tiedostoja" "Lähettäjän sijainti" + "Lähettänyt %1$s aikaan %2$s" "Aikarajoitettu toimenpide vaaditaan, sinulla on yksi minuutti aikaa vahvistaa" + "Asetukset, toimenpide vaaditaan" "Näytä salasana" "Aloita puhelu" "Aloita videopuhelu" "Aloita äänipuhelu" + "Viestiketju huoneessa %1$s" + "Viestiketjut huoneessa %1$s" "Haudattu huone" "Käyttäjän avatar" "Käyttäjävalikko" @@ -69,6 +77,7 @@ "Soita" "Peruuta" "Peruuta toistaiseksi" + "Valitse tiedosto" "Valitse kuva" "Tyhjennä" "Sulje" @@ -88,12 +97,16 @@ "Deaktivoi tili" "Hylkää" "Hylkää ja estä" + "Poista" + "Poista tili" + "Poista tiedosto" "Poista kysely" "Poista kaikki valinnat" "Poista käytöstä" "Hylkää" "Sulje" "Valmis" + "Lataa" "Muokkaa" "Muokkaa kuvatekstiä" "Muokkaa kyselyä" @@ -193,7 +206,6 @@ "Edistyneet asetukset" "kuva" "Analytiikka" - "Synkronoidaan ilmoituksia…" "Poistuit huoneesta" "Sinut kirjattiin ulos istunnosta" "Ulkoasu" @@ -201,7 +213,9 @@ "Beeta" "Estetyt käyttäjät" "Kuplat" + "Puhelu hylätty" "Puhelu alkoi" + "Hylkäsit puhelun" "Keskustelujen varmuuskopiointi" "Kopioitu leikepöydälle" "Tekijänoikeudet" @@ -211,7 +225,6 @@ "Poistuit huoneesta" "Poistuit tilasta" "Kutsu hylätty" - "Tumma" "Salauksen purkuvirhe" "Kuvaus" "Kehittäjän asetukset" @@ -237,6 +250,7 @@ Syy: %1$s." "Epäonnistui" "Lisää suosikkeihin" "Lisätty suosikkeihin" + "Synkronoidaan ilmoituksia…" "Tiedosto" "Tiedosto poistettu" "Tiedosto tallennettu" @@ -250,7 +264,6 @@ Syy: %1$s." "Tätä Matrix-tunnusta ei löytynyt, joten kutsu ei välttämättä mene perille." "Poistutaan huoneesta" "Poistutaan tilasta" - "Vaalea" "Rivi kopioitu leikepöydälle" "Linkki kopioitu leikepöydälle" "Yhdistä uusi laite" @@ -292,6 +305,7 @@ Syy: %1$s." "Odota hetki…" "Haluatko varmasti lopettaa tämän kyselyn?" "Kysely: %1$s" + "Kysely" "Ääniä yhteensä: %1$s" "Tulokset näkyvät kyselyn päätyttyä" @@ -317,7 +331,6 @@ Syy: %1$s." "%1$d vastausta" "Vastataan käyttäjälle %1$s" - "Ilmoita virheestä" "Ilmoita ongelmasta" "Ilmoitus lähetetty" "Rikastettu tekstieditori" @@ -371,7 +384,6 @@ Syy: %1$s." "Ehdotettu" "Ehdotukset" "Synkronoidaan" - "Järjestelmän oletus" "Teksti" "Kolmannen osapuolen ilmoitukset" "Viestiketju" @@ -460,6 +472,9 @@ Haluatko varmasti jatkaa?" "Anteeksi, tapahtui virhe" "🔐️ Liity seuraani %1$s -sovelluksessa" "Hei, keskustele kanssani %1$s -sovelluksessa: %2$s" + "Reaaliaikaisen sijainnin jakaminen" + "Sijainnin jakaminen käynnissä" + "%1$s Reaaliaikainen sijainti" "%1$s Android" "Raivostunut ravistaminen ilmoittaa virheestä" "Näyttökuva" @@ -530,4 +545,6 @@ Haluatko varmasti jatkaa?" "Sinulla ei ole oikeutta lukea tätä viestiä" "Viestin salauksen purkaminen ei onnistu" "Tämä viesti estettiin, koska et ole vahvistanut laitettasi tai koska lähettäjän on vahvistettava diigitaalinen identiteettisi." + "Laitteesi on liian vanha, tarvitset laitteen, jossa on Android 8 tai uudempi." + "Karttojen renderöintiä ei tueta" 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 08791b1b38..cbddbf5f08 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,5 +1,6 @@ + "Ajouter un avatar" "Ajouter une réaction: %1$s" "Adresse" "Avatar" @@ -9,13 +10,16 @@ "%1$d chiffre saisi" "%1$d chiffres saisis" + "Durée: %1$s" "Modifier l’avatar" "L’adresse complète sera %1$s" "Détails du chiffrement" "Augmenter la taille du composeur" "Masquer le mot de passe" + "Info" "Rejoindre l’appel" "Retourner à la fin de la conversation" + "Aller aux messages non lus" "Déplacer la carte vers ma position" "Mentions uniquement" "En sourdine" @@ -32,6 +36,7 @@ "Vitesse de lecture" "Sondage" "Sondage terminé" + "Position: %1$s" "Code QR" "Réagir avec %1$s" "Réagir avec d’autres émojis" @@ -47,11 +52,15 @@ "Avatar du salon" "Envoyer des fichiers" "Position de l’expéditeur" + "Envoyé par %1$s à %2$s" "Action limitée dans le temps requise, vous avez une minute pour effectuer la vérification" + "Paramètres, action requise" "Afficher le mot de passe" "Démarrer un appel" "Passer un appel vidéo" "Lancer un appel vocal" + "Discussion dans %1$s" + "Discussions dans %1$s" "Salon clôturé" "Avatar de l’utilisateur" "Menu utilisateur" @@ -69,6 +78,7 @@ "Appel" "Annuler" "Annuler pour l’instant" + "Choisir un fichier" "Choisir une photo" "Effacer" "Fermer" @@ -90,12 +100,14 @@ "Refuser et bloquer" "Supprimer" "Supprimer le compte" + "Supprimer le fichier" "Supprimer le sondage" "Tout désélectionner" "Désactiver" "Annuler" "Ignorer" "Terminé" + "Télécharger" "Modifier" "Modifier la légende" "Modifier le sondage" @@ -195,7 +207,6 @@ "Paramètres avancés" "une image" "Statistiques d’utilisation" - "Synchronisation des notifications…" "Vous avez quitté le salon" "Vous avez été déconnecté de la session" "Apparence" @@ -203,7 +214,9 @@ "Bêta" "Utilisateurs bloqués" "Bulles" + "Appel rejeté" "Appel démarré" + "Vous avez rejeté un appel" "Sauvegarde des discussions" "Copié dans le presse-papiers" "Droits d’auteur" @@ -213,7 +226,6 @@ "Vous avez quitté le salon" "Vous avez quitté l’espace" "Invitation refusée" - "Sombre" "Erreur de déchiffrement" "Description" "Options pour les développeurs" @@ -239,6 +251,7 @@ Raison : %1$s." "Échec" "Favori" "Ajouté aux favoris" + "Synchronisation des notifications…" "Fichier" "Fichier supprimé" "Fichier enregistré" @@ -252,7 +265,6 @@ Raison : %1$s." "Cet identifiant Matrix est introuvable, il est donc possible que l’invitation ne soit pas reçue." "Quitter le salon…" "En train de quitter l’espace" - "Clair" "Ligne copiée dans le presse-papiers" "Lien copié dans le presse-papiers" "Associer un nouvel appareil" @@ -294,6 +306,7 @@ Raison : %1$s." "Veuillez patienter…" "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" "Sondage : %1$s" + "Sondage" "Nombre total de votes : %1$s" "Les résultats s’afficheront une fois le sondage terminé" @@ -319,7 +332,6 @@ Raison : %1$s." "%1$d réponses" "En réponse à %1$s" - "Signaler un problème" "Remonter un problème" "Rapport soumis" "Éditeur de texte enrichi" @@ -373,7 +385,6 @@ Raison : %1$s." "Recommandé" "Suggestions" "Synchronisation" - "Système" "Texte" "Avis de tiers" "Fil de discussion" @@ -462,6 +473,9 @@ Raison : %1$s." "Désolé, une erreur s’est produite" "🔐️ Rejoignez-moi sur %1$s" "Salut, parle-moi sur %1$s : %2$s" + "Partage de position en continu" + "Partage de position en cours" + "%1$s position en temps réel" "%1$s Android" "Rageshake pour signaler un problème" "Capture d’écran" @@ -532,4 +546,6 @@ Raison : %1$s." "Vous ne pouvez pas voir ce message" "Impossible de déchiffrer le message" "Ce message a été bloqué soit parce que vous n’avez pas vérifié votre session, soit parce que l’expéditeur doit vérifier votre identité numérique." + "Votre appareil est trop ancien, un appareil fonctionnant sous Android 8 ou une version plus récente est requis." + "Affichage des cartes non supporté" diff --git a/libraries/ui-strings/src/main/res/values-hr/translations.xml b/libraries/ui-strings/src/main/res/values-hr/translations.xml index 61c9ea54e2..369d8ccce4 100644 --- a/libraries/ui-strings/src/main/res/values-hr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hr/translations.xml @@ -15,6 +15,7 @@ "Pojedinosti o šifriranju" "Proširi tekstno polje poruke" "Sakrij zaporku" + "Informacije" "Pridruži se pozivu" "Idi na dno" "Pomakni kartu na moju lokaciju" @@ -50,6 +51,7 @@ "Pošalji datoteke" "Lokacija pošiljatelja" "Potrebna je vremenski ograničena radnja, imate jednu minutu za potvrdu" + "Postavke, potrebna je radnja" "Prikaži zaporku" "Započni poziv" "Započni videopoziv" @@ -90,12 +92,15 @@ "Deaktiviraj račun" "Odbij" "Odbij i blokiraj" + "Izbriši" + "Izbriši račun" "Izbriši anketu" "Poništi sve odabire" "Onemogući" "Odbaci" "Odbaci" "Gotovo" + "Preuzmi" "Uredi" "Uredi opis" "Uredi anketu" @@ -195,7 +200,6 @@ "Napredne postavke" "slika" "Analitika" - "Sinkronizacija obavijesti…" "Napustili ste sobu" "Odjavljeni ste iz sesije" "Izgled" @@ -203,7 +207,9 @@ "Beta" "Blokirani korisnici" "Mjehurići" + "Poziv je odbijen" "Poziv je započeo" + "Odbili ste poziv" "Sigurnosna kopija razgovora" "Kopirano u međuspremnik" "Autorsko pravo" @@ -213,7 +219,6 @@ "Napustio/la je sobu" "Napušteni prostor" "Poziv je odbijen" - "Tamno" "Pogreška kod dešifriranja" "Opis" "Mogućnosti za razvojne inženjere" @@ -239,6 +244,7 @@ Razlog: %1$s ." "Nije uspjelo" "Favorit" "Označeno kao favorit" + "Sinkronizacija obavijesti…" "Datoteka" "Datoteka je izbrisana" "Datoteka je spremljena" @@ -252,7 +258,6 @@ Razlog: %1$s ." "Ovaj Matrix ID nije moguće pronaći, pa se pozivnica možda neće primiti." "Izlazak iz sobe" "Napuštanje prostora" - "Svijetlo" "Redak je kopiran u međuspremnik" "Poveznica je kopirana u međuspremnik." "Poveži novi uređaj" @@ -323,7 +328,6 @@ Razlog: %1$s ." "%1$d odgovora" "Odgovara korisniku %1$s" - "Prijavi pogrešku" "Prijavi problem" "Prijava je podnesena" "Uređivač obogaćenog teksta" @@ -379,7 +383,6 @@ Razlog: %1$s ." "Preporučeno" "Prijedlozi" "Sinkronizacija" - "Sustav" "Tekst" "Obavijesti trećih strana" "Nit" @@ -475,6 +478,14 @@ Jeste li sigurni da želite nastaviti?" "Mogućnosti" "Ukloni %1$s" "Postavke" + "Nitko ne dijeli svoju lokaciju" + "Dijeljenje lokacije uživo" + + "%1$d osoba" + "%1$d osoba" + "%1$d ljudi" + + "Na karti" "Odabir medija nije uspio, pokušajte ponovno." "Pritisnite poruku i odaberite “%1$s” kako biste uključili ovdje." "Prikvačite važne poruke kako bi ih se lakše moglo pronaći" 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 06f49eba93..131f6d5957 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -1,5 +1,6 @@ + "Profilkép hozzáadása" "Reakció hozzáadása: %1$s" "Cím" "Profilkép" @@ -9,13 +10,16 @@ "%1$d megadott számjegy" "%1$d megadott számjegy" + "Időtartam: %1$s" "Profilkép szerkesztése" "A teljes cím ez lesz: %1$s" "Titkosítás részletei" "Üzenet szövegmezőjének kibontása" "Jelszó elrejtése" + "Információ" "Csatlakozás a híváshoz" "Ugrás az aljára" + "Ugrás az olvasatlanhoz" "Térkép áthelyezése a jelenlegi helyre" "Csak megemlítések" "Némítva" @@ -32,6 +36,7 @@ "Lejátszási sebesség" "Szavazás" "Befejezett szavazás" + "Pozíció: %1$s" "QR-kód" "Reagálás a következővel: %1$s" "Reagálás más emodzsikkal" @@ -47,11 +52,15 @@ "Szoba profilképe" "Fájlküldés" "Felhasználó tartózkodási helye" + "%1$s küldte ekkor: %2$s" "Időkorlátos művelet szükséges, egy perce van az ellenőrzésre" + "Beállítások, beavatkozás szükséges" "Jelszó megjelenítése" "Hanghívás indítása" "Videohívás indítása" "Hanghívás indítása" + "Üzenetszál itt: %1$s" + "Üzenetszálak itt: %1$s" "Elévült szoba" "Felhasználói profilkép" "Felhasználói menü" @@ -69,6 +78,7 @@ "Hívás" "Mégse" "Egyelőre nem" + "Fájl kiválasztása" "Fénykép kiválasztása" "Törlés" "Bezárás" @@ -88,12 +98,16 @@ "Fiók deaktiválása" "Elutasítás" "Elutasítás és letiltás" + "Törlés" + "Fiók törlése" + "Fájl törlése" "Szavazás törlése" "Kijelölés megszüntetése" "Letiltás" "Elvetés" "Eltüntetés" "Kész" + "Letöltés" "Szerkesztés" "Felirat szerkesztése" "Szavazás szerkesztése" @@ -193,7 +207,6 @@ "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" @@ -201,7 +214,9 @@ "Béta" "Letiltott felhasználók" "Buborékok" + "Hívás elutasítva" "A hívás elindult" + "Elutasított egy hívást" "Csevegés biztonsági mentése" "A vágólapra másolva" "Szerzői jogok" @@ -211,7 +226,6 @@ "Elhagyta a szobát" "Tér elhagyva" "Meghívás elutasítva" - "Sötét" "Visszafejtési hiba" "Leírás" "Fejlesztői beállítások" @@ -237,6 +251,7 @@ Ok: %1$s." "Sikertelen" "Kedvenc" "Kedvencnek jelölve" + "Értesítések szinkronizálása…" "Fájl" "Fájl törölve" "A fájl mentve" @@ -250,7 +265,6 @@ Ok: %1$s." "Ez a Matrix-azonosító nem található, ezért előfordulhat, hogy a meghívó nem érkezik meg." "Szoba elhagyása" "Tér elhagyása" - "Világos" "A sor a vágólapra másolva" "Hivatkozás a vágólapra másolva" "Új eszköz összekapcsolása" @@ -292,6 +306,7 @@ Ok: %1$s." "Kis türelmet…" "Biztos, hogy befejezi ezt a szavazást?" "Szavazás: %1$s" + "Szavazás" "Összes szavazat: %1$s" "Az eredmények a szavazás befejezése után jelennek meg" @@ -316,7 +331,6 @@ Ok: %1$s." "%1$d válasz" "Válasz %1$s számára" - "Hiba jelentése" "Probléma jelentése" "A jelentés elküldve" "Formázott szöveges szerkesztő" @@ -370,7 +384,6 @@ Ok: %1$s." "Javaslat" "Javaslatok" "Szinkronizálás" - "Rendszer" "Szöveg" "Harmadik felek nyilatkozatai" "Üzenetszál" @@ -459,6 +472,9 @@ Biztos, hogy folytatja?" "Elnézést, hiba történt" "🔐️ Csatlakozz hozzám itt: %1$s" "Beszélgessünk itt: %1$s, %2$s" + "Élő földrajzi hely megosztása" + "Helymegosztás folyamatban" + "Élő hely: %1$s" "%1$s Android" "Az eszköz rázása a hibajelentéshez" "Képernyőkép" @@ -529,4 +545,6 @@ Biztos, hogy folytatja?" "Nincs hozzáférése ehhez az üzenethez" "Nem sikerült visszafejteni az üzenetet" "Ez az üzenet azért lett blokkolva, mert vagy nem ellenőrizte az eszközt, vagy a feladónak ellenőriznie kell az Ön digitális személyazonosságát." + "Az Ön eszköze túl régi, Android 8 vagy újabb verziójú eszközre van szükség." + "A térképek megjelenítése nem támogatott" diff --git a/libraries/ui-strings/src/main/res/values-in/translations.xml b/libraries/ui-strings/src/main/res/values-in/translations.xml index 5a095a074f..51d78530a8 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -144,7 +144,7 @@ "Bagikan tautan" "Tampilkan" "Masuk lagi" - "Keluar dari akun" + "Hapus device dari akun" "Keluar saja" "Lewati" "Mulai" @@ -185,7 +185,6 @@ "Permintaan dibatalkan" "Keluar dari ruangan" "Undangan ditolak" - "Gelap" "Kesalahan dekripsi" "Deskripsi" "Opsi pengembang" @@ -222,7 +221,6 @@ Alasan: %1$s." "Pasang APK" "ID Matrix ini tidak dapat ditemukan, sehingga undangan mungkin tidak diterima." "Meninggalkan ruangan" - "Terang" "Baris disalin ke papan klip" "Tautan disalin ke papan klip" "Memuat…" @@ -275,7 +273,6 @@ Alasan: %1$s." "%1$d balasan" "Membalas %1$s" - "Laporkan kutu" "Laporkan masalah" "Laporan terkirim" "Penyunting teks kaya" @@ -309,7 +306,6 @@ Alasan: %1$s." "Berhasil" "Saran" "Menyinkronkan" - "Sistem" "Teks" "Pemberitahuan pihak ketiga" "Utas" 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 824b9f6441..d1ec78d920 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -14,6 +14,7 @@ "Dettagli sulla crittografia" "Allarga il campo di testo del messaggio" "Nascondi password" + "Info" "Entra in chiamata" "Vai alla fine" "Sposta la mappa sulla mia posizione" @@ -48,6 +49,7 @@ "Invia file" "Posizione del mittente" "Azione richiesta a tempo limitato, hai un minuto per la verifica" + "Impostazioni, azione richiesta" "Mostra password" "Avvia una chiamata" "Avvia una videochiamata" @@ -69,6 +71,7 @@ "Chiama" "Annulla" "Annulla per ora" + "Scegli il file" "Scegli foto" "Cancella" "Chiudi" @@ -88,12 +91,15 @@ "Disattiva account" "Rifiuta" "Rifiuta e blocca" + "Elimina" + "Elimina account" "Elimina sondaggio" "Deseleziona tutti" "Disabilita" "Annulla" "Chiudi" "Fine" + "Scarica" "Modifica" "Modifica didascalia" "Modifica sondaggio" @@ -193,7 +199,6 @@ "Impostazioni avanzate" "un\'immagine" "Statistiche di utilizzo" - "Sincronizzazione delle notifiche…" "Hai lasciato la stanza" "Sei stato disconnesso dalla sessione" "Aspetto" @@ -201,7 +206,9 @@ "Beta" "Utenti bloccati" "Fumetti" + "Chiamata rifiutata" "Chiamata avviata" + "Hai rifiutato una chiamata" "Backup della chat" "Copiato negli appunti" "Copyright" @@ -211,7 +218,6 @@ "Hai lasciato la stanza" "Hai lasciato lo spazio" "Invito rifiutato" - "Scuro" "Errore di decrittazione" "Descrizione" "Opzioni sviluppatore" @@ -237,6 +243,7 @@ Motivo:. %1$s" "Fallito" "Preferiti" "Preferita" + "Sincronizzazione delle notifiche…" "File" "File eliminato" "File salvato" @@ -250,7 +257,6 @@ Motivo:. %1$s" "Questo ID Matrix non può essere trovato, quindi l\'invito potrebbe non essere ricevuto." "Lascio la stanza" "Uscendo dallo spazio" - "Chiaro" "Riga copiata negli appunti" "Collegamento copiato negli appunti" "Collega un nuovo dispositivo" @@ -317,7 +323,6 @@ Motivo:. %1$s" "%1$d risposte" "Risposta a %1$s" - "Segnala un problema" "Segnala un problema" "Segnalazione inviata" "Editor di testo avanzato" @@ -371,7 +376,6 @@ Motivo:. %1$s" "Suggeriti" "Suggerimenti" "Sincronizzazione" - "Sistema" "Testo" "Comunicazioni di terze parti" "Discussione" @@ -460,6 +464,8 @@ Sei sicuro di voler continuare?" "Siamo spiacenti, si è verificato un errore" "🔐️ Unisciti a me su %1$s" "Ehi, parliamo su %1$s: %2$s" + "Condivisione della posizione in corso" + "%1$s Posizione in tempo reale" "%1$s Android" "Scuoti per segnalare un problema" "Istantanea schermo" @@ -467,6 +473,13 @@ Sei sicuro di voler continuare?" "Risposte" "Rimuovi %1$s" "Impostazioni" + "Nessuno sta condividendo la propria posizione" + "Condivisione posizione in tempo reale" + + "%1$d persona" + "%1$d persone" + + "Sulla mappa" "Selezione del file multimediale fallita, riprova." "Premi su un messaggio e scegli “%1$s” per includerlo qui." "Fissa i messaggi importanti così che possano essere trovati facilmente" @@ -491,6 +504,7 @@ Sei sicuro di voler continuare?" "Messaggio in %1$s" "Espandi" "Riduci" + "Condivisione posizione in tempo reale" "Stai già visualizzando questa stanza!" "%1$s di %2$s" "%1$s Messaggi fissati" diff --git a/libraries/ui-strings/src/main/res/values-ja/translations.xml b/libraries/ui-strings/src/main/res/values-ja/translations.xml index ae5592d3f6..a9bdedc1c1 100644 --- a/libraries/ui-strings/src/main/res/values-ja/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ja/translations.xml @@ -8,13 +8,16 @@ "%1$d 桁入力済" + "長さ: %1$s" "アバターを編集" "完全なアドレスは %1$s になります。" "暗号化の詳細" "入力欄を拡大" "パスワードを非表示" + "情報" "通話に参加" "一番下へ" + "未読に移動" "現在地に移動" "メンションのみ" "ミュート有効" @@ -31,6 +34,7 @@ "再生速度" "投票" "投票終了" + "位置: %1$s" "QRコード" "リアクション: %1$s" "他の絵文字でリアクション" @@ -45,11 +49,15 @@ "ルームのアバター" "ファイルを送信" "送信者の位置情報" + "%2$s に %1$s が送信" "1分以内に検証を完了してください" + "処置が必要な設定" "パスワードを表示" "通話を開始" "ビデオ通話を開始" "音声通話を開始" + "%1$s のスレッド" + "%1$s のスレッド" "埋没したルーム" "ユーザーのアバター" "ユーザーメニュー" @@ -67,6 +75,7 @@ "通話" "キャンセル" "今回のみキャンセル" + "ファイルを選択" "写真を選択" "クリア" "閉じる" @@ -88,12 +97,14 @@ "拒否してブロック" "削除" "アカウントを削除" + "ファイルを削除" "投票を削除" "全ての選択を解除" "無効化" "破棄" "無視" "完了" + "ダウンロード" "編集" "キャプションを編集" "投票を編集" @@ -193,15 +204,16 @@ "高度な設定" "画像" "分析" - "通知を同期中…" "あなたがルームを退出" "セッションからログアウトされました" - "外観" + "テーマ" "音声" "ベータ版" "ブロックしたユーザー" "ふきだし" + "通話を拒否しました" "通話を開始しました" + "通話を拒否しました" "チャットをバックアップ" "クリップボードにコピーしました" "著作権" @@ -211,7 +223,6 @@ "ルームを退出しました" "スペースから退出しました" "招待は却下されました" - "ダーク" "復号化エラー" "詳細" "開発者向けオプション" @@ -237,6 +248,7 @@ "失敗" "お気に入り" "お気に入り" + "通知を同期中…" "ファイル" "ファイルを削除しました" "ファイルを保存しました" @@ -250,7 +262,6 @@ "このMatrix IDは見つからないため、招待が届かない可能性があります。" "ルームを退出しています" "スペースを退出しています" - "ライト" "行をクリップボードにコピーしました" "リンクをクリップボードにコピーしました" "新しい端末から接続" @@ -268,7 +279,7 @@ "メッセージアクション" "メッセージの送信に失敗" "メッセージのレイアウト" - "メッセージは削除されました" + "メッセージが削除されました" "モダン" "ミュート" "名前" @@ -282,7 +293,7 @@ "または" "他のオプション" "パスワード" - "人々" + "ユーザー" "固定リンク" "権限" "ピン留め" @@ -290,6 +301,7 @@ "お待ちください…" "本当に投票を終了しますか?" "投票: %1$s" + "投票" "総投票数:%1$s" "結果は投票終了後に表示されます" @@ -313,7 +325,6 @@ "%1$d 件の返信" "%1$s に返信" - "バグを報告" "問題を報告" "報告は送信されました" "リッチテキストエディター" @@ -364,7 +375,6 @@ "推奨される" "提案" "同期中" - "システム" "テキスト" "第三者に関する通知" "スレッド" @@ -453,6 +463,9 @@ "申し訳ありません。エラーが発生しました。" "🔐️ %1$s に参加してください" "%1$s で話しましょう: %2$s" + "ライブ位置情報共有" + "位置情報を共有しています" + "%1$s ライブ位置情報" "%1$s Android" "開発者にバグを報告するには端末を振ってください。" "スクリーンショット" diff --git a/libraries/ui-strings/src/main/res/values-ka/translations.xml b/libraries/ui-strings/src/main/res/values-ka/translations.xml index b7a8819391..2be4e43d61 100644 --- a/libraries/ui-strings/src/main/res/values-ka/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ka/translations.xml @@ -122,7 +122,6 @@ "საავტორო უფლება" "ოთახის შექმნა…" "დატოვა ოთახი" - "მუქი" "გაშიფვრის შეცდომა" "დეველოპერის პარამეტრები" "პირდაპირი ჩატი" @@ -145,7 +144,6 @@ "დააინსტალირეთ APK" "ეს Matrix ID ვერ მოიძებნა, ამიტომ მოწვევა შეიძლება არ იყოს მიღებული." "ოთახის დატოვება" - "ღია" "ბმული კოპირებულია გაცვლის ბუფერში" "იტვირთება…" @@ -186,7 +184,6 @@ "განახლება…" "პასუხი %1$s-ს" - "ხარვეზის შეტყობინება" "შეტყობინება პრობლემაზე" "რეპორტი გაგზავნილია" "მდიდარი ტექსტის რედაქტორი" @@ -213,7 +210,6 @@ "წარმატება" "შეთავაზებები" "სინქრონიზაცია" - "სისტემა" "ტექსტი" "მესამე პირის შენიშვნები" "თემა" 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 6a87870658..6336a8de92 100644 --- a/libraries/ui-strings/src/main/res/values-ko/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ko/translations.xml @@ -8,13 +8,16 @@ "%1$d자리 입력됨" + "소요 시간: %1$s" "아바타 편집" "전체 주소는 다음과 같습니다 %1$s" "암호화 세부 정보" "메시지 텍스트 필드 확장" "비밀번호 숨기기" + "정보" "통화 참가" "맨 아래로 이동" + "읽지 않은 항목으로 이동" "지도를 내 위치로 이동" "멘션만" "음소거함" @@ -31,6 +34,7 @@ "재생 속도" "투표" "종료된 투표" + "위치: %1$s" "QR 코드" "%1$s에 반응하세요" "다른 이모지로 반응하세요" @@ -45,10 +49,15 @@ "방 아바타" "파일 보내기" "발신자 위치" + "%1$s님이 %2$s에 보냈습니다." "제한 시간 내 인증이 필요합니다.1분 안에 확인해 주세요." + "설정,조치 필요" "비밀번호 표시" "통화 시작" + "영상 통화 시작" "음성 통화 시작" + "%1$s 내 스레드" + "%1$s 내 스레드 목록" "묘비 방" "사용자 아바타" "사용자 메뉴" @@ -66,6 +75,7 @@ "통화" "취소" "현재 취소" + "파일 선택" "사진 선택" "지우기" "닫기" @@ -85,12 +95,16 @@ "계정 비활성화" "거절" "거부 및 차단" + "삭제" + "계정 삭제" + "파일 삭제" "투표 삭제" "전체 선택 해제" "비활성화" "취소" "닫기" "완료" + "다운로드" "편집" "캡션 편집" "투표 수정" @@ -190,7 +204,6 @@ "고급 설정" "이미지" "통계" - "알림 동기화 중…" "방을 떠남" "세션에서 로그아웃되었습니다." "외관" @@ -198,7 +211,9 @@ "베타" "차단한 사용자" "버블" + "통화 거절" "통화 시작" + "통화를 거절하셨습니다" "채팅 백업" "클립보드에 복사됨" "저작권" @@ -208,7 +223,6 @@ "방 떠남" "스페이스 떠남" "초대 거부됨" - "다크" "복호화 오류" "설명" "개발자 설정" @@ -234,6 +248,7 @@ "실패" "즐겨찾기" "즐겨찾기 됨" + "알림 동기화 중…" "파일" "파일 삭제됨" "파일 저장됨" @@ -247,7 +262,6 @@ "Matrix ID를 찾을 수 없기 때문에 초대가 수신되지 않을 수도 있습니다." "방을 떠나는 중" "스페이스 떠나는 중" - "라이트" "줄이 클립보드에 복사되었습니다." "링크가 클립보드에 복사됨" "새 기기 연결" @@ -287,6 +301,7 @@ "기다려 주세요…" "정말로 이 투표를 종료하시겠어요?" "투표: %1$s" + "투표" "총 투표수: %1$s" "결과는 투표가 끝난 이후에 공개됨" @@ -310,7 +325,6 @@ "%1$d 답변" "%1$s님에게 답장하는 중" - "버그 보고" "문제 보고" "보고 제출됨" "리치 텍스트 편집기" @@ -361,7 +375,6 @@ "공유된 스페이스" "제안" "동기화 중" - "시스템" "글자" "제3자 고지" "스레드" @@ -452,6 +465,9 @@ "이런, 오류가 발생했어요" "🔐️ %1$s에 참여하기" "%1$s에서 대화해요: %2$s" + "실시간 위치 공유" + "위치 공유 중" + "%1$s님의 실시간 위치" "%1$s Android" "강하게 흔들어서 오류 보고하기" "스크린샷" @@ -459,6 +475,12 @@ "옵션" "%1$s 제거" "설정" + "위치를 공유 중인 사용자가 없습니다." + "실시간 위치 공유 중" + + "%1$d명" + + "지도 보기" "미디어 선택에 실패했습니다. 다시 시도해 주세요." "메시지를 누르고 \"%1$s\" 를 선택하여 여기에 포함합니다." "중요한 메시지를 고정하여 쉽게 찾을 수 있도록 합니다" @@ -482,6 +504,7 @@ "메시지 %1$s" "펼치기" "줄이다" + "실시간 위치 공유 중" "이 방을 이미 보고 있습니다!" "%2$s 의 %1$s" "%1$s 고정된 메시지" 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 280209f6a5..da5281212e 100644 --- a/libraries/ui-strings/src/main/res/values-lt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-lt/translations.xml @@ -126,7 +126,6 @@ "Reakcijos" "Atnaujinama…" "Atsakant %1$s" - "Pranešti apie klaidą" "Skundas pateiktas" "Kambario pavadinimas" "pvz., jūsų projekto pavadinimas" diff --git a/libraries/ui-strings/src/main/res/values-nb/translations.xml b/libraries/ui-strings/src/main/res/values-nb/translations.xml index fc641df34d..aa594f8027 100644 --- a/libraries/ui-strings/src/main/res/values-nb/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nb/translations.xml @@ -207,7 +207,6 @@ "Forlot rommet" "Forlot område" "Invitasjon avslått" - "Mørk" "Dekrypteringsfeil" "Beskrivelse" "Alternativer for utviklere" @@ -245,7 +244,6 @@ "Finner ikke denne Matrix-IDen, så invitasjonen blir kanskje ikke mottatt." "Forlater rommet" "Forlater området" - "Lys" "Linje kopiert til utklippstavlen" "Lenke kopiert til utklippstavlen" "Koble til ny enhet" @@ -309,7 +307,6 @@ "%1$d svar" "Svar til %1$s" - "Rapporter en feil" "Rapporter et problem" "Rapport sendt inn" "Redigeringsprogram for rik tekst" @@ -361,7 +358,6 @@ "Foreslått" "Forslag" "Synkroniserer" - "System" "Tekst" "Varsler fra tredjeparter" "Tråd" diff --git a/libraries/ui-strings/src/main/res/values-nl/translations.xml b/libraries/ui-strings/src/main/res/values-nl/translations.xml index 6e136e7d77..3edeeb00a0 100644 --- a/libraries/ui-strings/src/main/res/values-nl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml @@ -172,7 +172,6 @@ "Kamer maken…" "Heeft de kamer verlaten" "Uitnodiging geweigerd" - "Donker" "Decryptie fout" "Ontwikkelaarsopties" "Apparaat-ID" @@ -200,7 +199,6 @@ Reden: %1$s." "APK installeren" "Deze Matrix-ID kan niet worden gevonden, dus de uitnodiging is mogelijk niet ontvangen." "De kamer verlaten" - "Licht" "Link gekopieerd naar klembord" "Laden…" @@ -250,7 +248,6 @@ Reden: %1$s." "%1$d antwoorden" "Reageren op %1$s" - "Een fout melden" "Meld een probleem" "Melding ingediend" "Uitgebreide tekstverwerker" @@ -279,7 +276,6 @@ Reden: %1$s." "Geslaagd" "Suggesties" "Synchroniseren" - "Systeem" "Tekst" "Kennisgevingen van derden" "Gesprek" 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 6812885c9c..6136129893 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -1,5 +1,6 @@ + "Dodaj awatar" "Dodaj reakcję: %1$s" "Adres" "Awatar" @@ -10,13 +11,16 @@ "Wprowadzono %1$d cyfry" "Wprowadzono %1$d cyfr" + "Czas trwania: %1$s" "Edytuj awatar" "Podgląd pełnego adresu %1$s" "Szczegóły szyfrowania" "Powiększ pole tekstowe wiadomości" "Ukryj hasło" + "Informacje" "Dołącz do połączenia" "Przejdź na dół" + "Skocz do nieprzeczytanych" "Przesuń mapę do mojej lokalizacji" "Tylko wzmianki" "Wyciszone" @@ -27,11 +31,15 @@ "Strona %1$d" "Wstrzymaj" "Wiadomość głosowa, czas trwania: %1$s, aktualna pozycja: %2$s" + "Podgląd zdjęcia" "Pole PIN" "Przypięta lokalizacja" "Odtwórz" + "Prędkość odtwarzania" "Ankieta" "Zakończona ankieta" + "Pozycja: %1$s" + "Kod QR" "Zareaguj z %1$s" "Zareaguj innym emoji" "Odczytane przez %1$s i %2$s" @@ -46,12 +54,20 @@ "Usuń reakcję z %1$s" "Awatar pokoju" "Wyślij pliki" + "Lokalizacja nadawcy" + "Wysłane przez %1$s o %2$s" "Wymagane jest działanie ograniczone czasowo, została jedna minuta" + "Ustawienia, wymagane działanie" "Pokaż hasło" "Rozpocznij rozmowę" + "Rozpocznij rozmowę wideo" + "Rozpocznij połączenie głosowe" + "Wątek w %1$s" + "Wątki w %1$s" "Pokój nagrobkowy" "Awatar użytkownika" "Menu użytkownika" + "Podgląd wideo" "Wyświetl awatar" "Wyświetl szczegóły" "Wiadomość głosowa, czas trwania: %1$s" @@ -60,11 +76,13 @@ "Twój awatar" "Akceptuj" "Dodaj opis" + "Dodaj istniejące pokoje" "Dodaj do osi czasu" "Wróć" "Zadzwoń" "Anuluj" "Anuluj na razie" + "Wybierz plik" "Wybierz zdjęcie" "Wyczyść" "Zamknij" @@ -79,26 +97,33 @@ "Kopiuj tekst" "Utwórz" "Utwórz pokój" + "Utwórz przestrzeń" "Dezaktywuj" "Dezaktywuj konto" "Odrzuć" "Odrzuć i zablokuj" + "Usuń" + "Usuń konto" + "Usuń plik" "Usuń ankietę" "Odznacz wszystko" "Wyłącz" "Odrzuć" "Zamknij" "Gotowe" + "Pobierz" "Edytuj" "Edytuj opis" "Edytuj ankietę" "Włącz" "Zakończ ankietę" "Wprowadź PIN" + "Przeglądaj przestrzenie publiczne" "Zakończ" "Nie pamiętasz hasła?" "Przekaż dalej" "Wróć" + "Przejdź do ról i uprawnień" "Przejdź do ustawień" "Ignoruj" "Zaproś" @@ -114,7 +139,9 @@ "Opuść przestrzeń" "Załaduj więcej" "Zarządzaj kontem" + "Zarządzaj kontem i urządzeniami" "Zarządzaj urządzeniami" + "Zarządzaj pokojami" "Wiadomość" "Minimalizuj" "Dalej" @@ -139,7 +166,7 @@ "Zgłoś treść" "Zgłoś rozmowę" "Zgłoś pokój" - "Resetuj" + "Zresetuj" "Zresetuj tożsamość" "Spróbuj ponownie" "Ponów próbę odszyfrowania" @@ -152,18 +179,21 @@ "Wyślij wiadomość głosową" "Wyślij" "Wyślij link" + "Udostępnij lokalizację na żywo" "Pokaż" "Zaloguj się ponownie" - "Wyloguj" - "Wyloguj mimo to" + "Usuń to urządzenie" + "Usuń urządzenie mimo to" "Pomiń" "Rozpocznij" "Rozpocznij chat" "Zacznij od nowa" "Rozpocznij weryfikację" "Stuknij, aby załadować mapę" + "Zatrzymaj" "Zrób zdjęcie" "Stuknij, by wyświetlić opcje" + "Przetłumacz" "Spróbuj ponownie" "Odepnij" "Wyświetl" @@ -188,16 +218,19 @@ "Beta" "Zablokowani użytkownicy" "Bąbelki" + "Połączenie odrzucone" + "Trwa połączenie" "Rozpoczęto rozmowę" + "Odrzuciłeś połączenie" "Backup czatu" "Skopiowano do schowka" "Prawa autorskie" "Tworzenie pokoju…" + "Tworzenie przestrzeni…" "Anulowano żądanie" "Opuszczono pokój" "Opuścił przestrzeń" "Odrzucono zaproszenie" - "Ciemny" "Błąd deszyfrowania" "Opis" "Opcje programisty" @@ -213,6 +246,7 @@ "Pusty plik" "Szyfrowanie" "Szyfrowanie włączone" + "Kończy się o %1$s" "Wprowadź kod PIN" "Błąd" "Wystąpił błąd, możesz nie otrzymać powiadomień nowych wiadomości. Spróbuj naprawić powiadomienia w ustawieniach. @@ -222,6 +256,7 @@ Powód: %1$s." "Niepowodzenie" "Ulubione" "Ulubione" + "Synchronizuję powiadomienia…" "Plik" "Plik usunięty" "Plik zapisany" @@ -229,15 +264,18 @@ Powód: %1$s." "Przekaż wiadomość" "Często używane" "GIF" + "Trwa połączenie grupowe" "Zdjęcie" "W odpowiedzi do %1$s" "Zainstaluj APK" "Nie można znaleźć identyfikatora Matrix ID, zaproszenie mogło nie dotrzeć." "Opuszczanie pokoju" "Opuszczam przestrzeń" - "Jasny" "Wiersz skopiowany do schowka" "Link został skopiowany do schowka" + "Powiąż nowe urządzenie" + "Lokalizacja na żywo" + "Zakończono udostępnianie lokalizacji na żywo" "Ładowanie…" "Ładuję więcej…" @@ -252,10 +290,12 @@ Powód: %1$s." "Wiadomość" "Akcje wiadomości" + "Nie udało się wysłać wiadomości" "Układ wiadomości" "Wiadomość usunięta" "Nowoczesny" "Wycisz" + "Nazwa" "%1$s (%2$s)" "Brak wyników" "Brak nazwy pokoju" @@ -264,6 +304,7 @@ Powód: %1$s." "Offline" "Licencje open-source" "lub" + "Inne opcje" "Hasło" "Osoby" "Link bezpośredni" @@ -273,6 +314,7 @@ Powód: %1$s." "Proszę czekać…" "Jesteś pewien, że chcesz zakończyć tę ankietę?" "Ankieta: %1$s" + "Ankieta" "Łączna liczba głosów: %1$s" "Wyniki zostaną wyświetlone po zakończeniu ankiety" @@ -282,8 +324,10 @@ Powód: %1$s." "Przygotowuję…" "Polityka prywatności" + "Prywatny" "Pokój prywatny" "Prywatna przestrzeń" + "Publiczny" "Pokój publiczny" "Przestrzeń publiczna" "Reakcja" @@ -291,19 +335,20 @@ Powód: %1$s." "Powód" "Klucz przywracania" "Odświeżanie…" + "Usuwanie…" "%1$d odpowiedź" "%1$d odpowiedzi" "%1$d odpowiedzi" "Odpowiadanie do %1$s" - "Zgłoś błąd" "Zgłoś problem" "Zgłoszenie wysłane" "Bogaty edytor tekstu" + "Rola" "Pokój" "Nazwa pokoju" - "np. nazwa projektu" + "np. nazwa twojego projektu" "%1$d Pokój" "%1$d Pokoje" @@ -317,6 +362,11 @@ Powód: %1$s." "Bezpieczeństwo" "Wyświetlone przez" "Wybierz konto" + + "%1$d zaznaczony" + "%1$d zaznaczone" + "%1$d zaznaczonych" + "Wyślij do" "Wysyłanie…" "Błąd wysyłania" @@ -327,12 +377,16 @@ Powód: %1$s." "Adres URL serwera" "Ustawienia" "Udostępnij przestrzeń" + "Nowi członkowie widzą historię" + "Udostępniona lokalizacja na żywo" "Udostępniona lokalizacja" "Udostępniona przestrzeń" - "Wylogowywanie" + "Usuwanie urządzenia" "Coś poszło nie tak" "Napotkaliśmy problem. Spróbuj ponownie." "Przestrzeń" + "Członkowie przestrzeni" + "O czym jest ta przestrzeń?" "%1$d Przestrzeń" "%1$d Przestrzenie" @@ -341,12 +395,13 @@ Powód: %1$s." "Rozpoczynanie czatu…" "Naklejka" "Sukces" + "Polecane" "Sugestie" "Synchronizuję" - "System" "Tekst" "Informacje stron trzecich" "Wątek" + "Wątki" "Temat" "O czym jest ten pokój?" "Nie można odszyfrować" @@ -359,6 +414,7 @@ Powód: %1$s." "Wyłącz wyciszenie" "Nieobsługiwane połączenie" "Nieobsługiwane zdarzenie" + "%1$s rozpoczął połączenie" "Nazwa użytkownika" "Weryfikacja anulowana" "Weryfikacja zakończona" @@ -367,7 +423,7 @@ Powód: %1$s." "Zweryfikuj urządzenie" "Zweryfikuj tożsamość" "Zweryfikuj użytkownika" - "Film" + "Wideo" "Wysoka jakość" "Najlepsza jakość, większy rozmiar pliku" "Niska jakość" @@ -376,14 +432,20 @@ Powód: %1$s." "Balans między jakością a szybkością przesyłania" "Wiadomość głosowa" "Oczekiwanie…" - "Oczekiwanie na tę wiadomość" + "Oczekiwanie wiadomości" + "Czekam na lokalizację na żywo…" + "Każdy może zobaczyć historię" "Ty" - "Tożsamość %1$s została zresetowana. %2$s" + "%1$s (%2$s) udostępnił tę wiadomość, kiedy nie było Cię w pokoju, gdy została wysłana." + "%1$s udostępnił tę wiadomość, kiedy nie było Cię w pokoju, gdy została wysłana." + "Ten pokój został skonfigurowany tak, aby nowi członkowie mogli czytać historię czatu. %1$s" + "Tożsamość cyfrowa %1$s została zresetowana. %2$s" "Tożsamość %1$s %2$s została zresetowana. %3$s" "(%1$s)" - "Tożsamość %1$s została zresetowana" - "Tożsamość %1$s %2$s została zresetowana. %3$s" + "Tożsamość %1$s została zresetowana." + "Tożsamość %1$s %2$s została zresetowana. %3$s" "Wycofaj weryfikację" + "Zezwól na dostęp" "Link %1$s prowadzi Cię do innej witryny %2$s Czy na pewno chcesz kontynuować?" @@ -413,6 +475,7 @@ Czy na pewno chcesz kontynuować?" "%1$s nie mógł uzyskać dostępu do Twojej lokalizacji. Spróbuj ponownie później." "Nie udało się przesłać Twojej wiadomości głosowej." "Pokój już nie istnieje lub zaproszenie nie jest już ważne." + "Włącz GPS, aby uzyskać dostęp do funkcji opartych na lokalizacji." "Nie znaleziono wiadomości" "%1$s nie uzyskało uprawnienia do dostępu do twojej lokalizacji. Możesz włączyć dostęp w Ustawieniach." "%1$s nie ma uprawnień dostępu do Twojej lokalizacji. Włącz dostęp poniżej." @@ -424,6 +487,9 @@ Czy na pewno chcesz kontynuować?" "Przepraszamy, wystąpił błąd" "🔐️ Dołącz do mnie na %1$s" "Hej, porozmawiajmy na %1$s: %2$s" + "Udostępnianie lokalizacji na żywo" + "Udostępnianie lokalizacji w toku" + "Lokalizacja na żywo %1$s" "%1$s Android" "Wstrząśnij gniewnie, aby zgłosić błąd" "Zrzut ekranu" @@ -431,6 +497,14 @@ Czy na pewno chcesz kontynuować?" "Opcje" "Usuń %1$s" "Ustawienia" + "Nikt nie udostępnia swojej lokalizacji" + "Udostępnianie lokalizacji na żywo" + + "%1$d osoba" + "%1$d osoby" + "%1$d osób" + + "Na mapie" "Nie udało się wybrać multimediów. Spróbuj ponownie." "Naciśnij wiadomość i wybierz “%1$s”, aby dołączyć tutaj." "Przypinaj ważne wiadomości, aby można było je łatwo znaleźć" @@ -456,6 +530,7 @@ Czy na pewno chcesz kontynuować?" "Wiadomość w %1$s" "Rozwiń" "Zmniejsz" + "Udostępnianie lokalizacji na żywo" "Już oglądasz ten pokój!" "%1$s z %2$s" "%1$s przypiętych wiadomości" @@ -467,11 +542,20 @@ Czy na pewno chcesz kontynuować?" "Otwórz w Apple Maps" "Otwórz w Google Maps" "Otwórz w OpenStreetMap" - "Udostępnij tę lokalizację" + "Udostępnij wybraną lokalizację" + "Opcje udostępniania" "Przestrzenie, które stworzyłeś lub do których dołączyłeś." "%1$s • %2$s" + "Utwórz przestrzeń, aby organizować pokoje" "Przestrzeń %1$s" "Przestrzenie" + "Udostępnianie %1$s" + "Na mapie" + + "+ %1$d dołączył" + "+ %1$d dołączyło" + "+ %1$d dołączyło" + "Wiadomość nie została wysłana, ponieważ tożsamość %1$s została zresetowana." "Wiadomość nie została wysłana, ponieważ %1$s nie zweryfikował wszystkich urządzeń." "Wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń." @@ -482,5 +566,7 @@ Czy na pewno chcesz kontynuować?" "Musisz zweryfikować to urządzenie, aby uzyskać dostęp do historii wiadomości" "Nie masz uprawnień do tej wiadomości" "Nie można odszyfrować wiadomości" - "Wiadomość została zablokowana, ponieważ urządzenie nie zostało zweryfikowane lub nadawca musi zweryfikować Twoją tożsamość." + "Ta wiadomość została zablokowana, ponieważ urządzenie nie zostało zweryfikowane lub nadawca musi zweryfikować Twoją tożsamość." + "Twoje urządzenie jest przestarzałe, wymagany system Android 8 lub nowszy." + "Renderowanie map nie jest obsługiwane" diff --git a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml index 49351c6b64..f35aca5a58 100644 --- a/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt-rBR/translations.xml @@ -200,7 +200,6 @@ "Saiu da sala" "Saiu do espaço" "Convite recusado" - "Escuro" "Erro de descriptografia" "Descrição" "Opções de desenvolvedor" @@ -238,7 +237,6 @@ Motivo:​ %1$s." "Este ID Matrix não foi encontrado, então o convite pode não ser recebido" "Saindo da sala" "Saindo do espaço" - "Claro" "Linha copiada para a área de transferência" "Link copiado para área de transferência" "Vincular novo dispositivo" @@ -300,7 +298,6 @@ Motivo:​ %1$s." "%1$d respostas" "Respondendo a %1$s" - "Denunciar um bug" "Relatar um problema" "Relatório enviado" "Editor de rich text" @@ -351,7 +348,6 @@ Motivo:​ %1$s." "Sugerido" "Sugestões" "Sincronizando" - "Sistema" "Texto" "Comunicados de terceiros" "Tópico" diff --git a/libraries/ui-strings/src/main/res/values-pt/translations.xml b/libraries/ui-strings/src/main/res/values-pt/translations.xml index 56346fe1ac..0acc939565 100644 --- a/libraries/ui-strings/src/main/res/values-pt/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml @@ -152,8 +152,8 @@ "Partilhar ligação" "Mostrar" "Iniciar sessão novamente" - "Terminar sessão" - "Terminar mesmo assim" + "Remover este dispositivo" + "Remover mesmo assim" "Saltar" "Iniciar" "Iniciar conversa" @@ -195,7 +195,6 @@ "Saíste da sala" "Saíste do espaço" "Convite rejeitado" - "Escuro" "Erro de decifragem" "Descrição" "Opções de programador" @@ -233,7 +232,6 @@ Razão: %1$s." "Não foi possível encontrar este ID Matrix, portanto o convite pode não ser recebido." "A sair da sala" "A sair do espaço" - "Claro" "Linha copiada para a área de transferência" "Ligação copiada para a área de transferência" "A carregar…" @@ -291,7 +289,6 @@ Razão: %1$s." "%1$d respostas" "Em resposta a %1$s" - "Comunicar falha" "Comunicar um problema" "Denúncia submetida" "Editor de texto rico" @@ -322,7 +319,7 @@ Razão: %1$s." "Partilhar espaço" "Localização partilhada" "Espaço partilhado" - "A terminar sessão" + "A remover dispositivo" "Algo correu mal" "Encontramos um erro. Por favor, tenta novamente." "Espaço" @@ -335,7 +332,6 @@ Razão: %1$s." "Sucesso" "Sugestões" "A sincronizar…" - "Sistema" "Texto" "Avisos de terceiros" "Tópico" @@ -458,7 +454,7 @@ Tens a certeza de que queres continuar?" "Abrir no Apple Maps" "Abrir no Google Maps" "Abrir no OpenStreetMap" - "Partilhar este local" + "Partilhar local selecionado" "Espaços que criaste ou nos quais entraste." "%1$s • %2$s" "Espaço %1$s" diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index f47d43b47e..dfd68686ce 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -1,6 +1,7 @@ "Adăugați o reacție: %1$s" + "Adresă" "Imagine de profil" "Micșorați câmpul mesajului" "Ștergere" @@ -14,6 +15,7 @@ "Detalii privind criptarea" "Extindeți câmpul mesajului" "Ascundeți parola" + "Informații" "Alăturați-vă apelului" "Mergeți în jos" "Mutați harta la locația mea" @@ -27,9 +29,12 @@ "Pauză" "Mesaj vocal, durată:%1$s, poziție curentă: %2$s" "Câmp PIN" + "Locație fixată" "Redați" + "Viteză de redare" "Sondaj" "Sondaj încheiat" + "Cod QR" "Reacționați cu %1$s" "Reacționați cu alte emoji-uri" "Citit de %1$s și %2$s" @@ -44,9 +49,13 @@ "Îndepărtați reacția %1$s" "Avatarul camerei" "Trimiteți fișiere" + "Locația expeditorului" "Acțiune limitată în timp necesară, aveți un minut pentru a verifica" + "Setări, acțiune necesară" "Afișați parola" "Începeți un apel" + "Inițiați un apel video" + "Inițiați un apel vocal" "Cameră terminată" "Avatar utilizator" "Meniu utilizator" @@ -58,15 +67,17 @@ "Avatarul dumneavoastră" "Acceptați" "Adăugați o descriere" + "Adăugați camere existente" "Adăugați listei de mesaje" "Înapoi" - "Apel" + "Apelați" "Anulați" "Anulați pentru moment" + "Alegeți fișierul" "Alegeți o fotografie" "Ștergeți" "Închideți" - "Verificare completă" + "Finalizați verificarea" "Confirmați" "Confirmați parola" "Continuați" @@ -77,22 +88,27 @@ "Copiați textul" "Creați" "Creați o cameră" + "Creeați spațiu" "Dezactivați" "Dezactivați contul" "Refuzați" "Refuzați și blocați" + "Ștergere" + "Ștergeți contul" "Ștergeți sondajul" "Deselectați tot" "Dezactivați" "Renunţare" "Renunțați" - "Efectuat" + "Terminat" + "Descărcare" "Editați" "Editați descrierea" "Editați sondajul" "Activați" "Închideți sondajul" "Introduceți PIN-ul" + "Explorați spațiile publice" "Finalizați" "Ați uitat parola?" "Redirecționați" @@ -112,10 +128,11 @@ "Părăsiți camera" "Părăsiți spațiul" "Încărcați mai mult" - "Administrare cont" - "Gestionare dispozitive" + "Gestionați contul" + "Gestionați contul și dispozitivele" + "Gestionați dispozitivele" "Gestionați camerele" - "Mesaj" + "Contactați" "Minimizați" "Următorul" "Nu" @@ -124,11 +141,11 @@ "Deschideți meniul contextual" "Setări" "Deschideți cu" - "Fixează" + "Fixați" "Raspuns rapid" - "Citat" + "Citați" "Reacționați" - "Respinge" + "Respingeți" "Indepărtați" "Ștergeți descrierea" "Ștergeți mesajul" @@ -139,7 +156,7 @@ "Raportați conținutul" "Raportați conversația" "Raportați camera" - "Resetare" + "Resetați" "Resetați identitatea" "Reîncercați" "Reîncercați decriptarea" @@ -152,7 +169,8 @@ "Trimiteți un mesaj vocal" "Partajați" "Partajați linkul" - "Afișare" + "Partajați locația în timp real" + "Afișați" "Autentificați-vă din nou" "Deconectați-vă" "Deconectați-vă oricum" @@ -162,10 +180,12 @@ "Începeți din nou" "Începeți verificarea" "Atingeți pentru a încărca harta" + "Opriți" "Faceți o fotografie" "Atingeți pentru opțiuni" + "Traduceți" "Încercați din nou" - "Defixeaza" + "Defixați" "Vizualizați" "Vedeți în lista de mesaje" "Vedeți sursă" @@ -175,8 +195,8 @@ "Upgrade disponibil" "Despre" "Politică de utilizare rezonabilă" - "Adăugați un cont" - "Adăugați un alt cont" + "Adăugare cont" + "Adăugare cont" "Adăugare descriere" "Setări avansate" "o imagine" @@ -188,16 +208,18 @@ "Beta" "Utilizatori blocați" "Baloane" - "A început un apel" + "Apel respins" + "Apel inițiat" + "Ați respins un apel" "Backup conversații" "Copiat în clipboard" "Drepturi de autor" "Se creează camera…" + "Se crează spațiul" "Cerere anulată" - "Ați parăsit camera" - "S-a părăsit spațiul" + "Cameră părăsită" + "Spațiu părăsit" "Invitația a fost refuzată" - "Întunecat" "Eroare de decriptare" "Descriere" "Opțiuni programator" @@ -213,6 +235,7 @@ "Fișier gol" "Criptare" "Criptare activată" + "Se termină la %1$s" "Introduceți codul PIN" "Eroare" "A apărut o eroare, este posibil să nu primiți notificări pentru mesaje noi. Vă rugăm să depanați notificările din setări. @@ -222,6 +245,7 @@ Motiv:%1$s." "Eșuat" "Favorite" "Favorită" + "Se sincronizează notificările…" "Fişier" "Fișier șters" "Fișier salvat" @@ -235,10 +259,11 @@ Motiv:%1$s." "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost trimisă." "Se părăsește conversația" "Se părăsește spațiul" - "Deschis" "Linie copiată în clipboard" "Linkul a fost copiat în clipboard" "Conectați un dispozitiv nou" + "Locație în timp real" + "Locația în timp real s-a încheiat" "Se încarcă…" "Se încarcă…" @@ -257,7 +282,7 @@ Motiv:%1$s." "Aspectul mesajelor" "Mesaj șters" "Modern" - "Dezactivați sunetul" + "Dezactivare sunet" "Nume" "%1$s (%2$s)" "Niciun rezultat" @@ -267,6 +292,7 @@ Motiv:%1$s." "Deconectat" "Licențe open source" "sau" + "Alte opțiuni" "Parola" "Persoane" "Permalink" @@ -285,8 +311,10 @@ Motiv:%1$s." "Se pregăteşte…" "Politica de confidențialitate" + "Privat" "Cameră privată" "Spațiu privat" + "Public" "Cameră publică" "Spațiu public" "Reacţie" @@ -294,16 +322,17 @@ Motiv:%1$s." "Motiv" "Cheie de recuperare" "Se actualizează" + "Se elimină…" "%1$d răspuns" "%1$d răspunsuri" "%1$d răspunsuri" "Răspuns pentru %1$s" - "Raportați o eroare" "Raportați o problemă" "Raport trimis" "Editor text avansat" + "Rol" "Cameră" "Numele camerei" "de exemplu, numele proiectului dumneavoastră" @@ -320,6 +349,11 @@ Motiv:%1$s." "Securitate" "Văzut de" "Selectați un cont" + + "%1$d selectat" + "%1$d selectate" + "%1$d selectate" + "Trimiteți către" "Se trimite…" "Trimiterea a eșuat" @@ -329,13 +363,16 @@ Motiv:%1$s." "Serverul nu poate fi accesat" "Adresa URL a serverului" "Setări" - "Partajați spațiul" + "Partajare spațiu" + "Membrii noi pot vedea istoricul" + "Locație în timp real partajată" "Locație partajată" "Spațiu comun" "Eliminare în curs" "Ceva nu a mers bine" "Am întâmpinat o problemă. Vă rugăm să încercați din nou." "Spațiu" + "Membrii spațiului" "Despre ce este vorba în acest spațiu?" "%1$d Spațiu" @@ -345,12 +382,13 @@ Motiv:%1$s." "Se începe conversația…" "Autocolant" "Succes" + "Sugerat" "Sugestii" "Se sincronizează…" - "Sistem" "Text" "Notificări despre software de la terți" "Fir" + "Fire" "Subiect" "Despre ce este vorba în această cameră?" "Nu s-a putut decripta" @@ -360,8 +398,8 @@ Motiv:%1$s." "Nu am putut trimite invitații unuia sau mai multor utilizatori." "Nu s-a putut trimite invitația (invitațiile)" "Deblocare" - "Activați sunetul" - "Apel nesuportat" + "Activare sunet" + "Apel incompatibil" "Eveniment neacceptat" "Utilizator" "Verificare anulată" @@ -381,14 +419,19 @@ Motiv:%1$s." "Mesaj vocal" "Se aşteaptă…" "Mesaj în așteptare" + "Se așteptă localizarea în timp real…" + "Oricine poate vedea istoricul" "Dumneavoastră" + "%1$s (%2$s) a distribuit acest mesaj deoarece nu erați în cameră când a fost trimis." + "%1$s a distribuit acest mesaj deoarece nu erați în cameră când a fost trimis." "Această cameră a fost configurată astfel încât noii membri să poată citi istoricul. %1$s" "Identitatea digitala lui %1$s a fost resetată. %2$s" "Identitatea digitala %2$s a lui %1$s a fost resetată. %3$s" "(%1$s)" "Identitatea lui %1$s a fost resetată." - "Identitatea %2$s a lui %1$s a fost resetată. %3$s" + "Identitatea digitală %2$s a lui %1$s a fost resetată. %3$s" "Retrageți verificarea" + "Permiteți accesul" "Linkul %1$s vă redirecționează către un alt site %2$s Sunteți sigur că doriți să continuați?" @@ -418,6 +461,7 @@ Sunteți sigur că doriți să continuați?" "%1$s nu a putut accesa locația dumneavoastră. Vă rugăm să încercați din nou mai târziu." "Trimiterea mesajului vocal nu a reușit." "Camera nu mai există sau invitația nu mai este valabilă." + "Vă rugăm să activați GPS-ul pentru a accesa funcțiile bazate pe locație." "Mesajul nu a fost găsit" "%1$s nu are permisiuni pentru a accesa locația dumneavoastră. Puteți permite accesul în Setări." "%1$s nu are permisiuni pentru a accesa locația dumneavoastră. Permiteți accesul mai jos." @@ -429,6 +473,9 @@ Sunteți sigur că doriți să continuați?" "Ne pare rău, a apărut o eroare" "🔐️ Alăturați-vă mie în %1$s" "Hei, vorbește cu mine pe %1$s: %2$s" + "Partajarea locației în timp real" + "Partajarea locației este în curs de desfășurare" + "%1$s Locație în timp real" "%1$s Android" "Rageshake pentru a raporta erori" "Captură de ecran" @@ -436,6 +483,14 @@ Sunteți sigur că doriți să continuați?" "Opțiuni" "Ștergeți %1$s" "Setări" + "Nimeni nu își partajează locația" + "Se partajează locația în timp real" + + "%1$d persoană" + "%1$d persoane" + "%1$d persoane" + + "Pe hartă" "Selectarea fișierelor media a eșuat, încercați din nou." "Apăsați pe un mesaj și alegeți \"%1$s\" pentru a-l include aici." "Fixați mesajele importante, astfel încât să poată fi descoperite cu ușurință" @@ -461,6 +516,7 @@ Sunteți sigur că doriți să continuați?" "Mesaj în %1$s" "Extindeți" "Reduceți" + "Se partajează locația în timp real" "Deja vizualizați această cameră!" "%1$s din %2$s" "%1$s Mesaje fixate" @@ -473,10 +529,14 @@ Sunteți sigur că doriți să continuați?" "Deschideți în Google Maps" "Deschideți în OpenStreetMap" "Partajați locația selectată" + "Opțiuni de partajare" "Spații pe care le-ați creat sau la care v-ați alăturat." "%1$s • %2$s" + "Creați spații pentru a organiza camerele" "Spațiu %1$s" "Spații" + "Partajat %1$s" + "Pe hartă" "Mesajul nu a fost trimis deoarece identitatea digitala verificată a lui %1$s s-a schimbat." "Mesajul nu a fost trimis deoarece %1$s nu a verificat toate dispozitivele." "Mesajul nu a fost trimis deoarece nu ați verificat unul sau mai multe dispozitive." @@ -487,5 +547,5 @@ Sunteți sigur că doriți să continuați?" "Trebuie să verificați acest dispozitiv pentru a avea acces la mesajele anterioare." "Nu aveți acces la acest mesaj" "Nu s-a putut decripta mesajul" - "Acest mesaj a fost blocat fie pentru că nu ați verificat dispozitivul, fie pentru că expeditorul trebuie să vă verifice identitatea." + "Acest mesaj a fost blocat fie pentru că nu ați verificat dispozitivul, fie pentru că expeditorul trebuie să vă verifice identitatea digitală." 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 bb423b5b10..5c53c600d5 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -10,13 +10,16 @@ "Введено %1$d цифры" "Введено %1$d цифр" + "Продолжительность: %1$s" "Изменить аватар" "Полный адрес %1$s" "Сведения о шифровании" "Развернуть поле ввода" "Скрыть пароль" + "Информация" "Присоединиться к звонку" "Перейти вниз" + "Перейти к непрочитанным" "Переместить карту к моему местоположению" "Только упоминания" "Без звука" @@ -33,6 +36,7 @@ "Скорость воспроизведения" "Опрос" "Завершённый опрос" + "Позиция: %1$s" "QR-код" "Реагировать вместе с %1$s" "Реакция с помощью эмодзи" @@ -49,11 +53,15 @@ "Аватар комнаты" "Отправить файлы" "Местоположение отправителя" - "Требуется действие, на которое есть ограничение по времени, у вас есть одна минута для проверки" + "Отправлено пользователем %1$s в %2$s" + "Требуется действие, на которое есть ограничение по времени, у Вас есть одна минута для проверки" + "Настройки, требуется действие" "Показать пароль" "Начать звонок" "Начать видеозвонок" "Начать голосовой вызов" + "Обсуждение в %1$s" + "Обсуждения в %1$s" "Брошенная комната" "Аватар пользователя" "Меню пользователя" @@ -71,6 +79,7 @@ "Позвонить" "Отмена" "Пока отменить" + "Выберите файл" "Выбрать фото" "Очистить" "Закрыть" @@ -90,12 +99,16 @@ "Отключить учётную запись" "Отклонить" "Отклонить и заблокировать" + "Удалить" + "Удалить аккаунт" + "Удалить файл" "Удалить опрос" "Отменить выбор" "Отключить" "Отменить" "Закрыть" "Готово" + "Скачать" "Редактировать" "Изменить подпись" "Редактировать опрос" @@ -121,7 +134,7 @@ "Покинуть беседу" "Покинуть комнату" "Покинуть пространство" - "Загрузить еще" + "Загрузить ещё" "Настройки аккаунта" "Управление учетной записью и устройствами" "Управление устройствами" @@ -167,7 +180,7 @@ "Показать" "Повторить вход" "Удалить это устройство" - "Все равно удалить это устройство" + "Всё равно удалить это устройство" "Пропустить" "Начать" "Начать чат" @@ -184,8 +197,8 @@ "Просмотреть в хронологии" "Показать источник" "Да" - "Да, попробовать еще раз" - "Теперь ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола." + "Да, попробовать ещё раз" + "Теперь Ваш сервер поддерживает новый, более быстрый протокол. Чтобы обновить его прямо сейчас, выйдите и войдите в свою учётную запись снова. Сделав это сейчас, Вы сможете избежать принудительного выхода из системы при последующем удалении старого протокола." "Доступно обновление" "О приложении" "Политика допустимого использования" @@ -195,7 +208,6 @@ "Расширенные настройки" "изображение" "Аналитика" - "Синхронизация уведомлений…" "Вы покинули комнату" "Вы вышли из сессии" "Внешний вид" @@ -203,7 +215,9 @@ "Бета-версия" "Заблокированные пользователи" "Пузыри" + "Вызов отклонен" "Звонок начат" + "Ты отклонил звонок" "Резервная копия чатов" "Скопировано в буфер обмена" "Авторское право" @@ -213,7 +227,6 @@ "Покинул комнату" "Покинуть пространство" "Приглашение отклонено" - "Темная" "Ошибка расшифровки" "Описание" "Для разработчиков" @@ -239,6 +252,7 @@ "Ошибка" "Избранное" "Избранное" + "Синхронизация уведомлений…" "Файл" "Файл удалён" "Файл сохранен" @@ -252,7 +266,6 @@ "Идентификатор Matrix ID не найден, приглашение может быть не получено." "Покидаем комнату" "Покидаем пространство" - "Светлое" "Строка скопирована в буфер обмена" "Ссылка скопирована в буфер обмена" "Привязать новое устройство" @@ -296,6 +309,7 @@ "Подождите…" "Вы действительно хотите завершить данный опрос?" "Опрос: %1$s" + "Опрос" "Всего голосов: %1$s" "Результаты будут показаны после завершения опроса" @@ -323,7 +337,6 @@ "%1$d ответов" "Отвечает %1$s" - "Сообщить об ошибке" "Сообщить о проблеме" "Отчет отправлен" "Форматирование" @@ -365,7 +378,7 @@ "Общее пространство" "Удаление устройства" "Что-то пошло не так" - "Мы столкнулись с проблемой. Пожалуйста, попробуйте еще раз." + "Мы столкнулись с проблемой. Пожалуйста, попробуйте ещё раз." "Пространство" "Участники пространства" "О чём это пространство?" @@ -380,7 +393,6 @@ "Рекомендуемые" "Предложения" "Синхронизация" - "Системное" "Текст" "Уведомления о третьих лицах" "Обсуждение" @@ -418,8 +430,8 @@ "Ожидание трансляции местоположения…" "Любой может видеть историю" "Вы" - "%1$s (%2$s) поделился этим сообщением, поскольку вас не было в комнате, когда оно было отправлено." - "%1$s поделился этим сообщением, поскольку вас не было в комнате, когда оно было отправлено." + "%1$s (%2$s) поделился(ась) этим сообщением, поскольку Вас не было в комнате, когда оно было отправлено." + "%1$s поделился(ась) этим сообщением, поскольку Вас не было в комнате, когда оно было отправлено." "Эта комната настроена так, что новые участники могут видеть историю. %1$s" "Личность %1$s была сброшена. %2$s" "Личность %1$s %2$s была сброшена. %3$s" @@ -442,11 +454,11 @@ "Ошибка" "Успешно" "Предупреждение" - "У вас есть несохраненные изменения." + "У Вас есть несохраненные изменения." "Изменения не сохранены. Вы действительно хотите вернуться?" "Сохранить изменения?" "Максимально допустимый размер файла: %1$s" - "Выберите качество для видео, которое вы хотите загрузить." + "Выберите качество видео, которое Вы хотите загрузить." "Выберите качество загружаемого видео" "Поиск эмодзи" "Вы уже вошли на данном устройстве как %1$s." @@ -469,6 +481,9 @@ "Произошла ошибка" "🔐️ Присоединяйтесь ко мне в %1$s" "Привет, давай поболтаем в %1$s: %2$s" + "Отправка местонахождения в реальном времени" + "Определение местоположения в процессе" + "Текущее местоположение %1$s" "%1$s Android" "Встряхните устройство, чтобы сообщить об ошибке" "Скриншот" @@ -484,7 +499,7 @@ "%1$d людей" "На карте" - "Не удалось выбрать медиа, попробуйте еще раз." + "Не удалось выбрать медиа, попробуйте ещё раз." "Нажмите на сообщение и выберите «%1$s», чтобы добавить его сюда." "Закрепите важные сообщения, чтобы их можно было легко найти" @@ -492,19 +507,19 @@ "%1$d закреплённых сообщения" "%1$d закреплённых сообщений" - "Закрепленные сообщения" + "Закреплённые сообщения" "Вы собираетесь перейти в свой аккаунт %1$s, чтобы сбросить личность. После этого Вы вернётесь в приложение." "Не можете подтвердить? Перейдите в свой аккаунт, чтобы сбросить свою личность." "Сбросить верификацию и отправить" "Вы можете либо сбросить подтверждение и всё равно отправить это сообщение, либо отменить его сейчас и повторить попытку после повторного подтверждения %1$s." "Ваше сообщение не было отправлено, потому что подтвержденная идентификации %1$s была сброшена" - "Все равно отправить сообщение" - "У %1$s есть одно или несколько неподтвержденных устройств. Вы все равно можете отправить сообщение или отменить его пока и повторить попытку позже, когда %2$s подтвердить все свои устройства." + "Всё равно отправить сообщение" + "У %1$s есть одно или несколько неподтвержденных устройств. Вы всё равно можете отправить сообщение или отменить его и повторить попытку позже, когда %2$s подтвердит все свои устройства." "Ваше сообщение не было отправлено, потому что %1$s имеет неподтвержденные устройства" "Одно или несколько ваших устройств не подтверждены. Вы можете отправить сообщение в любом случае или отменить его пока и повторить попытку позже, подтвердив все свои устройства." "Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств." "Редактировать владельцев и администраторов" - "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." + "Не удалось обработать медиафайл для загрузки, попробуйте ещё раз." "Не удалось получить данные о пользователе" "Сообщение в %1$s" "Развернуть" @@ -512,9 +527,9 @@ "Поделиться текущим местоположением…" "Эта комната уже просматривается!" "%1$s из %2$s" - "%1$s закрепленные сообщения" + "%1$s Закреплённые сообщения" "Загрузка сообщения…" - "Посмотреть все" + "Посмотреть Все" "Чат" "Поделиться местоположением" "Поделиться моим местоположением" @@ -541,4 +556,6 @@ "Вы не имеете доступа к этому сообщению" "Не удалось расшифровать сообщение" "Это сообщение было заблокировано либо потому, что вы не подтвердили свое устройство, либо потому, что отправителю необходимо подтвердить вашу цифровую личность." + "Ваше устройство слишком старое, требуется устройство с Android 8 или более новой версией." + "Отображение карт не поддерживается" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index e154093ddb..11f4b22005 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -1,6 +1,7 @@ "Pridať reakciu: %1$s" + "Adresa" "Obrázok" "Minimalizovať textové pole správy" "Vymazať" @@ -27,6 +28,7 @@ "Pozastaviť" "Hlasová správa, dĺžka:%1$s, aktuálna pozícia: %2$s" "Pole PIN" + "Pripnuté miesto" "Prehrať" "Anketa" "Ukončená anketa" @@ -47,6 +49,8 @@ "Vyžaduje sa časovo obmedzená akcia, na overenie máte jednu minútu" "Zobraziť heslo" "Začať hovor" + "Začať videohovor" + "Začať hlasový hovor" "Opustená miestnosť" "Profilový obrázok" "Používateľské menu" @@ -157,8 +161,8 @@ "Zdieľať odkaz" "Zobraziť" "Prihláste sa znova" - "Odhlásiť sa" - "Napriek tomu sa odhlásiť" + "Odstrániť toto zariadenie" + "Napriek tomu odstrániť toto zariadenie" "Preskočiť" "Spustiť" "Začať konverzáciu" @@ -202,7 +206,6 @@ "Opustil/a miestnosť" "Opustil priestor" "Pozvánka bola odmietnutá" - "Tmavý" "Chyba dešifrovania" "Popis" "Možnosti pre vývojárov" @@ -240,7 +243,6 @@ Dôvod: %1$s." "Toto Matrix ID sa nedá nájsť, takže pozvánka nemusí byť prijatá." "Opustenie miestnosti" "Opúšťanie priestoru" - "Svetlý" "Riadok skopírovaný do schránky" "Odkaz bol skopírovaný do schránky" "Prepojiť nové zariadenie" @@ -306,7 +308,6 @@ Dôvod: %1$s." "%1$d odpovedí" "Odpoveď na %1$s" - "Nahlásiť chybu" "Nahlásiť problém" "Nahlásenie bolo odoslané" "Rozšírený textový editor" @@ -360,7 +361,6 @@ Dôvod: %1$s." "Navrhované" "Návrhy" "Synchronizuje sa" - "Systém" "Text" "Oznámenia tretích strán" "Vlákno" @@ -487,7 +487,7 @@ Naozaj chcete pokračovať?" "Otvoriť v Apple Maps" "Otvoriť v Mapách Google" "Otvoriť v OpenStreetMap" - "Zdieľajte túto polohu" + "Zdieľajte vybranú polohu" "Priestory, ktoré ste vytvorili alebo ku ktorým ste sa pripojili." "%1$s • %2$s" "Vytvorte priestory na usporiadanie miestností" 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 dfcd514209..28dd63d425 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -187,7 +187,6 @@ "Begäran avbruten" "Lämnade rummet" "Inbjudan avvisad" - "Mörkt" "Avkrypteringsfel" "Beskrivning" "Utvecklaralternativ" @@ -224,7 +223,6 @@ Anledning:%1$s." "Installera APK" "Det här Matrix-ID:t kan inte hittas, så inbjudan kanske inte tas emot." "Lämnar rummet" - "Ljust" "Rad kopierad till klippbordet" "Länk kopierad till klippbordet" "Laddar …" @@ -281,7 +279,6 @@ Anledning:%1$s." "%1$d svar" "Svarar till %1$s" - "Rapportera en bugg" "Rapportera ett problem" "Rapport inskickad" "Riktextredigerare" @@ -323,7 +320,6 @@ Anledning:%1$s." "Lyckades" "Förslag" "Synkar" - "System" "Text" "Meddelanden från tredje part" "Tråd" diff --git a/libraries/ui-strings/src/main/res/values-tr/translations.xml b/libraries/ui-strings/src/main/res/values-tr/translations.xml index d92daa6527..d500b57507 100644 --- a/libraries/ui-strings/src/main/res/values-tr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-tr/translations.xml @@ -175,7 +175,6 @@ "İstek iptal edildi" "Sol oda" "Davet reddedildi" - "Koyu" "Şifre çözme hatası" "Geliştirici seçenekleri" "Cihaz Kimliği" @@ -211,7 +210,6 @@ Neden: %1$s." "APK\'yı yükleyin" "Bu Matrix Kimliği bulunamıyor, bu nedenle davet alınmayabilir." "Odadan ayrılma" - "Aydınlık" "Metin panoya kopyalandı" "Bağlantı panoya kopyalandı" "Yükleniyor…" @@ -264,7 +262,6 @@ Neden: %1$s." "Kurtarma anahtarı" "Yenileniyor…" "Cevaplamak için %1$s" - "Hata bildir" "Sorun bildir" "Rapor gönderildi" "Zengin metin editörü" @@ -296,7 +293,6 @@ Neden: %1$s." "Başarılı" "Öneriler" "Senkronizasyon" - "Sistem" "Metin" "Üçüncü taraf bildirimleri" "Konu" diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index 835ded33f9..a65d2ebc14 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -1,6 +1,7 @@ "Додати реакцію: %1$s" + "Адреса" "Аватар" "Згорнути поле тексту повідомлення" "Видалити" @@ -9,13 +10,16 @@ "Введено %1$d цифри" "Введено %1$d цифр" + "Тривалість: %1$s" "Редагувати аватар" "Повна адреса буде %1$s" "Подробиці шифрування" "Розгорнути текстове поле повідомлення" "Cховати пароль" + "Інформація" "Приєднатися до виклику" "Перейти вниз" + "Перейти до непрочитаних" "Перемістити карту до мого місця перебування" "Тільки згадки" "Звук вимкнено" @@ -27,9 +31,13 @@ "Пауза" "Голосове повідомлення, тривалість: %1$s, поточна позиція: %2$s" "Поле PIN-коду" + "Закріплена локація" "Відтворити" + "Швидкість програвання" "Опитування" "Опитування завершено" + "Позиція: %1$s" + "QR-код" "Реагувати з%1$s" "Відреагувати іншими смайликами" "Прочитано %1$s та %2$s" @@ -44,9 +52,16 @@ "Прибрати реакцію %1$s" "Аватар кімнати" "Надіслати файли" + "Локація відправника" + "Надіслано %1$s о %2$s" "Необхідно виконати дію, обмежену в часі, у вас є одна хвилина для верифікації" + "Налаштування, потрібні дії" "Показати пароль" "Розпочати виклик" + "Розпочати відеодзвінок" + "Розпочати голосовий дзвінок" + "Гілка в %1$s" + "Гілки в %1$s" "Кімната більше не використовується" "Аватар користувача" "Меню користувача" @@ -64,6 +79,7 @@ "Зателефонувати" "Скасувати" "Скасувати наразі" + "Вибрати файл" "Вибрати фото" "Очистити" "Закрити" @@ -83,18 +99,23 @@ "Деактивувати обліковий запис" "Відхилити" "Відхилити та заблокувати" + "Видалити" + "Видалити обліковий запис" + "Видалити файл" "Видалити опитування" "Скасувати вибір усіх" "Вимкнути" "Відкинути" "Відхилити" "Готово" + "Завантажити" "Редагувати" "Редагувати підпис" "Редагувати опитування" "Увімкнути" "Завершити опитування" "Введіть PIN-код" + "Дізнатися про публічні простори" "Завершити" "Забули пароль?" "Переслати" @@ -115,6 +136,7 @@ "Вийти з простору" "Завантажити ще" "Керування обліковим записом" + "Керування обліковим записом і пристроями" "Керування пристроями" "Керувати кімнатами" "Написати" @@ -154,6 +176,7 @@ "Надіслати голосове повідомлення" "Поділитися" "Поділитися посиланням" + "Ділитися місцезнаходженням у реальному часі" "Показати" "Увійдіть знову" "Вийти" @@ -164,6 +187,7 @@ "Почати спочатку" "Почати верифікацію" "Натисніть, щоб завантажити мапу" + "Зупинити" "Зробити фото" "Торкніться, щоб переглянути параметри" "Перекласти" @@ -191,7 +215,9 @@ "Бета-версія" "Заблоковані користувачі" "Бульбашки" + "Дзвінок відхилено" "Виклик розпочато" + "Ви відхилили дзвінок" "Резервне копіювання бесіди" "Скопійовано до буферу обміну" "Авторське право" @@ -199,8 +225,8 @@ "Створення простору…" "Запит скасовано" "Виходить з кімнати" + "Вийшов із простору" "Запрошення відхилено" - "Темна" "Помилка розшифрування" "Опис" "Налаштування розробника" @@ -216,6 +242,7 @@ "Порожній файл" "Шифрування" "Шифрування ввімкнено" + "Завершується о %1$s" "Введіть свій PIN-код" "Помилка" "Сталася помилка, ви можете не отримувати сповіщення про нові повідомлення. Усуньте неполадки зі сповіщеннями в налаштуваннях. @@ -225,6 +252,7 @@ "Помилка" "Обране" "Обране" + "Синхронізація сповіщень…" "Файл" "Файл видалено" "Файл збережено" @@ -238,10 +266,11 @@ "Цей Matrix-ID не знайдено, тому запрошення може не бути отримано." "Вихід з кімнати" "Вихід з простору" - "Світла" "Рядок скопійовано до буфера обміну" "Посилання скопійовано в буфер обміну" "Під\'єднати новий пристрій" + "Місцезнаходження в реальному часі" + "Показ місцеперебування наживо завершено" "Завантаження" "Завантаження наступних…" @@ -270,6 +299,7 @@ "Не в мережі" "Ліцензії відкритого коду" "або" + "Інші варіанти" "Пароль" "Люди" "Постійне посилання" @@ -279,6 +309,7 @@ "Будь ласка, зачекайте…" "Ви впевнені, що хочете закінчити це опитування?" "Опитування: %1$s" + "Опитування" "Всього голосів: %1$s" "Результати будуть показані після завершення опитування" @@ -288,8 +319,10 @@ "Приготування…" "Політика конфіденційності" + "Приватний" "Приватна кімната (тільки за запрошенням)" "Приватний простір" + "Публічний" "Загальнодоступна кімната" "Загальнодоступний простір" "Реакція" @@ -304,7 +337,6 @@ "%1$d відповідей" "Відповідь %1$s" - "Повідомити про ваду" "Повідомити про проблему" "Звіт подано" "Багатоформатний текстовий редактор" @@ -325,6 +357,11 @@ "Безпека" "Переглянули" "Вибрати обліковий запис" + + "%1$d вибраний" + "%1$d вибрані" + "%1$d вибрано" + "Надіслати до" "Надсилання…" "Не вдалося надіслати" @@ -334,12 +371,16 @@ "Сервер недоступний" "URL-адреса сервера" "Налаштування" + "Поділитися простором" "Нові учасники бачать історію" + "Поділитися місцезнаходженням в реальному часі" "Поширене розташування" + "Простір спільного користування" "Вихід" "Щось пішло не так" "Ми зіткнулися з проблемою. Будь ласка, повторіть спробу." "Простір" + "Учасники простору" "Про що цей простір?" "%1$d простір" @@ -349,12 +390,13 @@ "Початок бесіди…" "Наліпка" "Успіх" + "Запропоновано" "Пропозиції" "Синхронізація" - "Системна" "Текст" "Повідомлення третіх сторін" "Гілка" + "Гілки" "Тема" "Про що ця кімната?" "Неможливо розшифрувати" @@ -385,14 +427,19 @@ "Голосове повідомлення" "Очікування…" "Чекаємо на це повідомлення" + "Очікування геолокації…" "Будь-хто може переглянути історію" "Ви" + "%1$s (%2$s) поділився цим повідомленням, оскільки вас не було в кімнаті, коли його надіслали." + "%1$s поділився цим повідомленням, оскільки вас не було в кімнаті, коли його надіслали." + "Згідно із налаштувань цієї кімнати, нові учасники можуть переглядати історію. %1$s" "Ідентичність %1$s скинуто. %2$s" "Ідентичність %1$s %2$s скинуто. %3$s" "(%1$s)" "Ідентичність %1$s скинуто." "Ідентичність %1$s %2$s скинуто. %3$s" "Відкликати верифікацію" + "Надати доступ" "Посилання %1$s спрямовує вас на інший сайт %2$s Ви впевнені, що хочете продовжити?" @@ -422,6 +469,7 @@ "%1$s не вдалося отримати доступ до вашого розташування. Повторіть спробу пізніше." "Не вдалося завантажити голосове повідомлення." "Кімната більше не існує або запрошення не чинне." + "Будь ласка, увімкніть GPS, щоб отримати доступ до функцій, що використовують геолокацію." "Повідомлення не знайдено" "%1$s не має дозволу на доступ до вашого розташування. Увімкнути доступ можна в Налаштуваннях." "%1$s не має дозволу на доступ до вашого розташування. Увімкніть доступ нижче." @@ -433,6 +481,9 @@ "Вибачте, сталася помилка" "🔐️ Приєднуйтеся до мене в %1$s" "Вітаю, поспілкуйтеся зі мною в %1$s: %2$s" + "Обмін місцезнаходженням у реальному часі" + "Триває обмін місцезнаходженням" + "%1$s Місцезнаходження в реальному часі" "%1$s Android" "Повідомити про ваду за допомогою Rageshake" "Знімок екрана" @@ -440,6 +491,14 @@ "Варіанти" "Вилучити %1$s" "Налаштування" + "Ніхто не ділиться своєю геопозицією" + "Обмін геопозицією" + + "%1$d особа" + "%1$d осіб" + "%1$d осіб" + + "На карті" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Натисніть на повідомлення і виберіть \"%1$s\", щоб додати його сюди." "Закріпіть важливі повідомлення, щоб їх можна було легко знайти" @@ -465,6 +524,7 @@ "Повідомлення в %1$s" "Розгорнути" "Згорнути" + "Спільний доступ до місцезнаходження у реальному часі" "Уже переглядаєте цю кімнату!" "%1$s із %2$s" "%1$s закріплених повідомлень" @@ -477,10 +537,14 @@ "Відкрити в Картах Google" "Відкрити в OpenStreetMap" "Поділитися цим місцем перебування" + "Налаштування обміну геопозицією" "Простори, які ви створили або до яких приєдналися." "%1$s • %2$s" + "Створіть простори для організації кімнат" "Простір %1$s" "Простори" + "Надано доступ %1$s" + "На карті" "Повідомлення не надіслано, оскільки підтверджену особистість %1$s скинуто." "Повідомлення не надіслано, оскільки %1$s перевірив не всі пристрої." "Повідомлення не надіслано, оскільки ви не підтвердили один або кілька своїх пристроїв." diff --git a/libraries/ui-strings/src/main/res/values-ur/translations.xml b/libraries/ui-strings/src/main/res/values-ur/translations.xml index 8e9c0fb3be..accba4556c 100644 --- a/libraries/ui-strings/src/main/res/values-ur/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ur/translations.xml @@ -141,7 +141,6 @@ "حقوقِ طبع و نشر" "کمرہ تخلیق کررہاہے…" "کمرہ چھوڑ لیا" - "اندھیرا" "رمزکشائی کی خرابی" "مطور اختیارات" "براہ راست گفتگو" @@ -168,7 +167,6 @@ "APK تنصیب کریں" "یہ میٹرکس شناخت نہیں مل سکتی، تو ہو سکتا ہے کہ دعوت نامہ موصول نہ ہو۔" "کمرہ چھوڑنا" - "روشنی" "ربط تختہ تراشہ پر نقل کردا گیا" "لاد رہا ہے…" @@ -212,7 +210,6 @@ "بازیابی کی کلید" "تاکہ کر رہا ہے…" "%1$s کا جواب دے رہے ہیں" - "ایک خطاء کی اطلاع دیں" "کسی مسئلے کی اطلاع دیں" "گزارش جمع ہوگئی" "امیر مدیرِ متن" @@ -241,7 +238,6 @@ "کامیابی" "تجاویز" "ہمسات سازی" - "نظام" "متن" "فریق ثالث کے اشعارات" "دھاگہ" diff --git a/libraries/ui-strings/src/main/res/values-uz/translations.xml b/libraries/ui-strings/src/main/res/values-uz/translations.xml index 2542de084e..63e1756a23 100644 --- a/libraries/ui-strings/src/main/res/values-uz/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uz/translations.xml @@ -193,7 +193,6 @@ "Kengaytirilgan sozlamalar" "rasm" "Analitika" - "Bildirishnomalar sinxronlanmoqda…" "Siz xonani tark etdingiz" "Siz sessiyadan chiqdingiz" "Ko\'rinish" @@ -211,7 +210,6 @@ "Xonani tark etdi" "Tar etilgan maydon" "Taklif rad etildi" - "Tungi" "Shifrni ochish xatosi" "Tavsif" "Dasturchi variantlari" @@ -237,6 +235,7 @@ Sababi:%1$s." "Xatolikka uchradi" "Sevimli" "Sevimli" + "Bildirishnomalar sinxronlanmoqda…" "Fayl" "Fayl o\'chirildi" "Fayl saqlandi" @@ -250,7 +249,6 @@ Sababi:%1$s." "Ushbu Matrix identifikatori topilmadi, shuning uchun taklif qabul qilinmasligi mumkin." "Xonadan chiqish" "Maydonni tark etish" - "Nur" "Satr vaqtinchalik xotiraga nusxalandi" "Havola vaqtinchalik xotiraga nusxalandi" "Yangi qurilmani ulang" @@ -317,7 +315,6 @@ Sababi:%1$s." "%1$d ta javob" "%1$sga Javob berilmoqda" - "Xato haqida xabar bering" "Muammo haqida xabar bering" "Hisobot topshirildi" "Boy matn muharriri" @@ -371,7 +368,6 @@ Sababi:%1$s." "Tavsiya etilgan" "Tavsiyalar" "Sinxronlash" - "Tizim" "Matn" "Uchinchi tomon bildirishnomalari" "Ip" @@ -410,6 +406,7 @@ Sababi:%1$s." "Tarixni hamma ko‘rishi mumkin" "Siz" "%1$s (%2$s) bu xabarni ulashdi, chunki u yuborilganda siz xonada emas edingiz." + "%1$s bu xabar yuborilgan paytda siz xonada bo‘lmaganingiz uchun uni ulashdi." "Siz yuborgan xabarlar bu xonaga taklif qilingan yangi a’zolarga ulashiladi. %1$s" "%1$sning raqamli identifikatori qayta tiklandi.%2$s" "%1$sning%2$s raqamli identifikatsiya qayta tiklandi.%3$s" diff --git a/libraries/ui-strings/src/main/res/values-vi/translations.xml b/libraries/ui-strings/src/main/res/values-vi/translations.xml index 9a450117ac..5cfbe26fba 100644 --- a/libraries/ui-strings/src/main/res/values-vi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-vi/translations.xml @@ -191,7 +191,6 @@ "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" @@ -209,7 +208,6 @@ "Đã 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" @@ -235,6 +233,7 @@ Lý do: %1$s ." "Thất bại" "Yêu thích" "Được yêu thích" + "Đang đồng bộ thông báo…" "Tập tin" "Tệp đã bị xóa" "Tệp đã được lưu" @@ -248,7 +247,6 @@ Lý do: %1$s ." "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" @@ -310,7 +308,6 @@ Lý do: %1$s ." "%1$d trả lờ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" @@ -360,7 +357,6 @@ Lý do: %1$s ." "Gợi ý" "Gợi ý" "Đang đồng bộ" - "Hệ thống" "Văn bản" "Thông báo từ bên thứ ba" "Chủ đề" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 11eb111b49..71159b78e4 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -191,7 +191,6 @@ "進階設定" "影像" "分析" - "正在同步通知……" "您離開聊天室" "您已登出工作階段" "外觀" @@ -209,7 +208,6 @@ "已離開聊天室" "離開空間" "邀請被拒絕" - "深色" "解密錯誤" "描述" "開發者選項" @@ -235,6 +233,7 @@ "失敗" "我的最愛" "我的最愛" + "正在同步通知……" "檔案" "檔案已刪除" "檔案已儲存" @@ -248,7 +247,6 @@ "找不到此 Matrix ID,因此可能沒有人會收到邀請。" "正在離開聊天室" "離開空間" - "淺色" "行已複製到剪貼簿" "連結已複製到剪貼簿" "連結新裝置" @@ -311,7 +309,6 @@ "%1$d 個回覆" "正在回覆%1$s" - "回報程式錯誤" "回報問題" "已遞交報告" "格式化文字編輯器" @@ -362,7 +359,6 @@ "已建議" "建議" "同步中" - "系統" "文字" "第三方通知" "討論串" 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 7bd218ca54..ac73cbed68 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -1,5 +1,6 @@ + "添加头像" "添加反应:%1$s" "地址" "头像" @@ -8,13 +9,16 @@ "已输入 %1$d 个数字" + "持续时间:%1$s" "编辑头像" "完整地址为 %1$s" "加密详情" "展开消息文本框" "隐藏密码" + "信息" "加入通话" "跳转到底部" + "跳转到未读" "将地图移动到我的位置" "仅提及" "通知已关闭" @@ -25,12 +29,14 @@ "第 %1$d 页" "暂停" "语音消息,时长:%1$s,当前位置:%2$s" + "照片预览" "PIN 字段" "已钉住的位置" "播放" "播放速度" "投票" "投票已结束" + "位置:%1$s" "二维码" "使用 %1$s 反应" "使用其它 Emoji 做出反应" @@ -40,19 +46,23 @@ "%1$s 已读" "点击以显示全部" - "撤回反应 %1$s" + "移除反应:%1$s" "移除反应:%1$s" "房间头像" "发送文件" "发送方位置" + "由 %1$s 发送于 %2$s" "请求的操作有时间限制,你有 1 分钟的时间来验证" "显示密码" "开始通话" "开始视频通话" "开始语音通话" + "位于 %1$s 中的消息列" + "位于 %1$s 中的消息列" "已封存的房间" "用户头像" "用户菜单" + "视频预览" "查看头像" "查看详情" "语音消息,时长:%1$s" @@ -67,6 +77,7 @@ "通话" "取消" "暂时取消" + "选择文件" "选择照片" "清除" "关闭" @@ -88,6 +99,7 @@ "拒绝并屏蔽" "删除" "删除账户" + "删除文件" "删除投票" "取消全选" "禁用" @@ -110,9 +122,9 @@ "前往设置" "忽略" "邀请" - "邀请朋友" - "邀请别人加入 %1$s" - "邀请别人加入 %1$s" + "邀请人员" + "邀请人员加入 %1$s" + "邀请人员加入 %1$s" "邀请" "加入" "了解更多" @@ -125,7 +137,7 @@ "管理账户与设备" "管理设备" "管理房间" - "发送消息给" + "发送消息" "最小化" "下一步" "否" @@ -145,7 +157,7 @@ "回复" "在消息列中回复" "举报" - "报告错误" + "报告 bug" "举报内容" "举报对话" "举报房间" @@ -165,8 +177,8 @@ "共享实时位置" "显示" "再次登录" - "删除此设备" - "仍要删除此设备" + "移除此设备" + "仍要移除此设备" "跳过" "开始" "开始聊天" @@ -190,11 +202,10 @@ "可接受的使用政策" "添加账户" "添加账户" - "添加标题" + "正在添加标题" "高级设置" "一张图片" "分析" - "正在同步通知…" "你离开了房间" "你已注销会话" "外观" @@ -203,6 +214,7 @@ "已屏蔽用户" "气泡" "来电被拒接" + "通话进行中" "通话已开始" "你已拒接来电" "聊天记录备份" @@ -210,11 +222,10 @@ "版权" "正在创建房间…" "正在创建空间…" - "请求已取消" - "离开房间" + "申请已取消" + "已离开房间" "离开空间" "邀请已拒绝" - "深色" "解密错误" "描述" "开发者选项" @@ -225,7 +236,7 @@ "正在下载" "(已编辑)" "正在编辑" - "编辑标题" + "正在编辑标题" "* %1$s %2$s" "空文件" "加密" @@ -238,8 +249,9 @@ 原因:%1$s。" "所有人" "失败" - "收藏夹" + "收藏" "已收藏" + "正在同步通知…" "文件" "文件已删除" "文件已保存" @@ -247,13 +259,13 @@ "转发消息" "常用" "GIF" + "群组通话进行中" "图片" "回复 %1$s" "安装 APK" "找不到此 Matrix ID,因此可能无法收到邀请。" "正在离开房间" "正在离开空间" - "浅色" "链接已复制到剪贴板" "链接已复制到剪贴板" "关联新设备" @@ -285,7 +297,7 @@ "或" "其它选项" "密码" - "用户" + "人员" "永久链接" "权限" "已置顶" @@ -293,6 +305,7 @@ "请稍候…" "确定要结束这个投票吗?" "投票:%1$s" + "投票" "总票数:%1$s" "结果将在投票结束后显示" @@ -316,7 +329,6 @@ "%1$d 个回复" "正在回复 %1$s" - "报告 bug" "报告问题" "报告已提交" "富文本编辑器" @@ -367,7 +379,6 @@ "建议" "建议" "正在同步" - "系统" "文本" "第三方通知" "消息列" @@ -384,6 +395,7 @@ "解除静音" "不受支持的通话" "不支持的事件" + "%1$s 开始了通话" "用户名" "验证已取消" "验证完成" @@ -408,11 +420,11 @@ "由于你当时不在房间内,%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(%2$s)的数字身份已重置。%3$s" "撤回验证" "允许访问" "链接 %1$s 将跳转至外部网站 %2$s @@ -424,7 +436,7 @@ "允许的最大文件大小为:%1$s" "文件太大,无法上传" "已举报房间" - "举报并离开房间" + "已举报并离开房间" "确认" "错误" "成功" @@ -440,7 +452,7 @@ "你的主服务器需要升级,以支持 Matrix 认证服务和账户创建。" "永久链接创建失败" "%1$s 无法加载地图,请稍后再试。" - "加载消息失败" + "消息加载失败" "%1$s 无法访问你的位置,请稍后再试。" "无法上传语音消息。" "该房间已不存在或邀请已失效。" @@ -456,6 +468,9 @@ "抱歉,发生了错误" "🔐️ 在 %1$s 中与我一起" "嗨!请通过 %1$s 与我联系:%2$s" + "正在共享实时位置" + "位置共享正在进行" + "%1$s 实时位置" "%1$s Android" "摇一摇以报告 bug" "屏幕截图" @@ -495,7 +510,7 @@ "共享实时位置" "已经位于此房间!" "%1$s / %2$s" - "置顶消息 %1$s" + "%1$s 个已置顶的消息" "正在加载消息…" "查看全部" "聊天" @@ -511,17 +526,22 @@ "创建空间以组织房间" "空间 %1$s" "空间" - "共享于%1$s" + "已共享 %1$s" "在地图上" - "消息未发送,因为%1$s的已验证数字身份已被重置。" + + "+ %1$d 人已加入" + + "消息未能发送,因为 %1$s 的已验证数字身份已被重置。" "消息未能发送,因为 %1$s 尚未验证所有设备。" "消息未能发送,因为你有尚未验证的设备。" "位置" - "版本:%1$s (%2$s)" + "版本:%1$s(%2$s)" "zh-Hans" "历史消息在此设备上不可用" "你需要验证此设备才能访问历史消息" "无权访问此消息" "无法解密消息" "此消息已被阻止,因为你未验证你的设备,或发送者需要验证你的数字身份。" + "你的设备过旧,需要 Android 8 或更高版本的设备。" + "地图渲染不受支持。" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 9db9bb4751..ece51775e5 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ + "Add avatar" "Add reaction: %1$s" "Address" "Avatar" @@ -9,6 +10,7 @@ "%1$d digit entered" "%1$d digits entered" + "Duration: %1$s" "Edit avatar" "The full address will be %1$s" "Encryption details" @@ -17,6 +19,7 @@ "Info" "Join call" "Jump to bottom" + "Jump to unread" "Move the map to my location" "Mentions only" "Muted" @@ -27,12 +30,14 @@ "Page %1$d" "Pause" "Voice message, duration: %1$s, current position: %2$s" + "Photo preview" "PIN field" "Pinned location" "Play" "Playback speed" "Poll" "Ended poll" + "Position: %1$s" "QR Code" "React with %1$s" "React with other emojis" @@ -48,15 +53,19 @@ "Room avatar" "Send files" "Sender location" + "Sent by %1$s at %2$s" "Time limited action required, you have one minute to verify" "Settings, action required" "Show password" "Start a call" "Start a video call" "Start a voice call" + "Thread in %1$s" + "Threads in %1$s" "Tombstoned room" "User avatar" "User menu" + "Video preview" "View avatar" "View details" "Voice message, duration: %1$s" @@ -71,6 +80,7 @@ "Call" "Cancel" "Cancel for now" + "Choose file" "Choose photo" "Clear" "Close" @@ -92,6 +102,7 @@ "Decline and block" "Delete" "Delete account" + "Delete file" "Delete Poll" "Deselect all" "Disable" @@ -198,7 +209,6 @@ "Advanced settings" "an image" "Analytics" - "Syncing notifications…" "You left the room" "You were logged out of the session" "Appearance" @@ -207,6 +217,7 @@ "Blocked users" "Bubbles" "Call declined" + "Call in progress" "Call started" "You declined a call" "Chat backup" @@ -218,7 +229,6 @@ "Left room" "Left space" "Invite declined" - "Dark" "Decryption error" "Description" "Developer options" @@ -244,6 +254,7 @@ Reason: %1$s." "Failed" "Favourite" "Favourited" + "Syncing notifications…" "File" "File deleted" "File saved" @@ -251,13 +262,13 @@ Reason: %1$s." "Forward message" "Frequently used" "GIF" + "Group call in progress" "Image" "In reply to %1$s" "Install APK" "This Matrix ID can\'t be found, so the invite might not be received." "Leaving room" "Leaving space" - "Light" "Line copied to clipboard" "Link copied to clipboard" "Link new device" @@ -299,6 +310,7 @@ Reason: %1$s." "Please wait…" "Are you sure you want to end this poll?" "Poll: %1$s" + "Poll" "Total votes: %1$s" "Results will show after the poll has ended" @@ -324,7 +336,6 @@ Reason: %1$s." "%1$d replies" "Replying to %1$s" - "Report a bug" "Report a problem" "Report submitted" "Rich text editor" @@ -378,7 +389,6 @@ Reason: %1$s." "Suggested" "Suggestions" "Syncing" - "System" "Text" "Third-party notices" "Thread" @@ -395,6 +405,7 @@ Reason: %1$s." "Unmute" "Unsupported call" "Unsupported event" + "%1$s started a call" "Username" "Verification cancelled" "Verification complete" @@ -467,6 +478,9 @@ Are you sure you want to continue?" "Sorry, an error occurred" "🔐️ Join me on %1$s" "Hey, talk to me on %1$s: %2$s" + "Live Location Sharing" + "Location sharing in progress" + "%1$s Live Location" "%1$s Android" "Rageshake to report bug" "Screenshot" @@ -526,6 +540,10 @@ Are you sure you want to continue?" "Spaces" "Shared %1$s" "On the map" + + "+ %1$d joined" + "+ %1$d joined" + "Message not sent because %1$s’s verified digital identity was reset." "Message not sent because %1$s has not verified all devices." "Message not sent because you have not verified one or more of your devices." @@ -538,4 +556,6 @@ Are you sure you want to continue?" "You don\'t have access to this message" "Unable to decrypt message" "This message was blocked either because you did not verify your device or because the sender needs to verify your digital identity." + "Your device is too old, a device with Android 8 or newer is required." + "Rendering maps is not supported" diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml deleted file mode 100644 index ba6c431d8b..0000000000 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - "Black" - diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/HasExternalKeyboard.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/HasExternalKeyboard.kt new file mode 100644 index 0000000000..ed35cfbc3e --- /dev/null +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/HasExternalKeyboard.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.libraries.ui.utils.a11y + +import android.app.Activity +import android.app.Application +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun hasExternalKeyboard(): Boolean { + val activity = requireNotNull(LocalActivity.current) + var hasExternalKeyboard by remember { mutableStateOf(activity.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + DisposableEffect(Unit) { + val callback = object : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) { + // We do not have access to onActivityConfigurationChanged, so update the value when tha Activity is resumed + hasExternalKeyboard = activity.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS + } + + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + } + activity.registerActivityLifecycleCallbacks(callback) + onDispose { + activity.unregisterActivityLifecycleCallbacks(callback) + } + } + } + return hasExternalKeyboard +} diff --git a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/IsTalkbackEnabled.kt similarity index 96% rename from libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt rename to libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/IsTalkbackEnabled.kt index 60ac1887c6..938a774355 100644 --- a/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/time/IsTalkbackEnabled.kt +++ b/libraries/ui-utils/src/main/kotlin/io/element/android/libraries/ui/utils/a11y/IsTalkbackEnabled.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.ui.utils.time +package io.element.android.libraries.ui.utils.a11y import android.view.accessibility.AccessibilityManager import androidx.compose.runtime.Composable diff --git a/libraries/voiceplayer/api/build.gradle.kts b/libraries/voiceplayer/api/build.gradle.kts index f37c263d83..e058210b7d 100644 --- a/libraries/voiceplayer/api/build.gradle.kts +++ b/libraries/voiceplayer/api/build.gradle.kts @@ -16,5 +16,6 @@ android { dependencies { implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) } diff --git a/libraries/voiceplayer/impl/build.gradle.kts b/libraries/voiceplayer/impl/build.gradle.kts index 4aa00e188b..8fe79fb774 100644 --- a/libraries/voiceplayer/impl/build.gradle.kts +++ b/libraries/voiceplayer/impl/build.gradle.kts @@ -21,6 +21,7 @@ setupDependencyInjection() dependencies { api(projects.libraries.voiceplayer.api) + implementation(projects.libraries.architecture) implementation(projects.libraries.audio.api) implementation(projects.libraries.core) implementation(projects.libraries.di) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt index 9d471f7a6d..9d41b0da92 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.voicerecorder.impl.audio import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Provider import io.element.android.libraries.di.RoomScope import io.element.android.opusencoder.OggOpusEncoder import timber.log.Timber @@ -20,7 +19,7 @@ import java.io.File */ @ContributesBinding(RoomScope::class) class DefaultEncoder( - private val encoderProvider: Provider, + private val encoderProvider: () -> OggOpusEncoder, config: AudioConfig, ) : Encoder { private val bitRate = config.bitRate diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphrase.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphrase.kt new file mode 100644 index 0000000000..c7bc602dde --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphrase.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.libraries.wellknown.api + +/** + * Server-driven settings for a user-chosen recovery passphrase, advertised under the well-known + * `custom_recovery_passphrase` key. Today the only rule is a minimum character count; additional + * settings can be added here as the schema grows. + */ +data class CustomRecoveryPassphrase( + val minCharacterCount: Int, +) { + /** True when [input] meets every active rule. */ + fun isSatisfiedBy(input: String): Boolean = isSatisfiedBy(input.length) + + /** True when an input of [length] characters meets every active rule. */ + fun isSatisfiedBy(length: Int): Boolean = length >= minCharacterCount +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index 134a9bcdb5..8f2c07ab68 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -14,4 +14,6 @@ data class ElementWellKnown( val rageshakeUrl: String?, val brandColor: String?, val notificationSound: String?, + val identityProviderAppScheme: String?, + val customRecoveryPassphrase: CustomRecoveryPassphrase?, ) diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index f803eeec3c..1e2c4d7d61 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -33,9 +33,13 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) + implementation(projects.libraries.cachestore.api) + implementation(projects.services.toolbox.api) testCommonDependencies(libs) testImplementation(libs.coroutines.core) + testImplementation(projects.libraries.cachestore.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.wellknown.test) testImplementation(projects.services.toolbox.test) } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index 3bcf9bf573..a0223e93cc 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -10,29 +10,70 @@ package io.element.android.libraries.wellknown.impl import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.SessionWellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import timber.log.Timber @ContributesBinding(SessionScope::class) class DefaultSessionWellknownRetriever( private val matrixClient: MatrixClient, private val json: JsonProvider, + private val cacheStore: CacheStore, + private val systemClock: SystemClock, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, ) : SessionWellknownRetriever { private val domain by lazy { matrixClient.userIdServerName() } override suspend fun getElementWellKnown(): WellknownRetrieverResult { val url = "https://$domain/.well-known/element/element.json" + val cacheData = cacheStore.getData(url) + if (cacheData != null) { + Timber.d("Element .well-known data retrieved from cache for $domain") + // If the cache is outdated, trigger a refresh in background but still return the cached value + if (systemClock.epochMillis() > cacheData.updatedAt + CACHE_VALIDITY_MILLIS) { + sessionCoroutineScope.launch { + fetchElementWellKnown(url) + } + } + try { + val parsed = json().decodeFromString(cacheData.value).map() + return WellknownRetrieverResult.Success(parsed) + } catch (e: Exception) { + Timber.e(e, "Failed to parse cached Element .well-known data for $domain, deleting cache") + cacheStore.deleteData(url) + } + } + + return fetchElementWellKnown(url) + } + + private suspend fun fetchElementWellKnown(url: String): WellknownRetrieverResult { return matrixClient .getUrl(url) .mapCatchingExceptions { val data = String(it) - json().decodeFromString(data).map() + val parsed = json().decodeFromString(data).map() + // Also store in cache, if valid + cacheStore.storeData( + key = url, + data = CacheData( + value = data, + updatedAt = systemClock.epochMillis(), + ) + ) + parsed } .toWellknownRetrieverResult() } @@ -51,4 +92,9 @@ class DefaultSessionWellknownRetriever( } } ) + + companion object { + // 1 day + private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L + } } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index d4661d1be0..cd0f39c77f 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -15,7 +15,15 @@ import kotlinx.serialization.Serializable * Example: *
  * {
- *     "registration_helper_url": "https://element.io"
+ *     "registration_helper_url": "https://element.io",
+ *     "enforce_element_pro": true,
+ *     "rageshake_url": "https://example.org/rageshake",
+ *     "brand_color": "#FF0000",
+ *     "notification_sound": "ring.flac",
+ *     "idp_app_scheme": "io.element.app",
+ *     "custom_recovery_passphrase": {
+ *         "min_character_count": 8
+ *     }
  * }
  * 
* . @@ -32,4 +40,14 @@ data class InternalElementWellKnown( val brandColor: String? = null, @SerialName("notification_sound") val notificationSound: String? = null, + @SerialName("idp_app_scheme") + val identityProviderAppScheme: String? = null, + @SerialName("custom_recovery_passphrase") + val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null, +) + +@Serializable +data class InternalCustomRecoveryPassphrase( + @SerialName("min_character_count") + val minCharacterCount: Int? = null, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index c7ca088e68..c8760881d4 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -8,7 +8,19 @@ package io.element.android.libraries.wellknown.impl +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase import io.element.android.libraries.wellknown.api.ElementWellKnown +import timber.log.Timber + +private val loggerTag = LoggerTag("Wellknown") + +/** + * Floor for the recovery passphrase minimum length. A value of 1 guarantees the derived passphrase is + * never empty (an empty passphrase derives a recoverable but effectively unprotected secret-storage + * key). Anything stronger than non-empty is left to the homeserver operator's configuration. + */ +private const val MINIMUM_PASSPHRASE_CHARACTER_COUNT = 1 internal fun InternalElementWellKnown.map() = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, @@ -16,4 +28,21 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( rageshakeUrl = rageshakeUrl, brandColor = brandColor, notificationSound = notificationSound, + identityProviderAppScheme = identityProviderAppScheme, + customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(), ) + +private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase { + // Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring + // the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count + // or advertises a non-positive value. The operator owns any stronger minimum. + val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT) + if (min != minCharacterCount) { + Timber.tag(loggerTag.value).w( + "custom_recovery_passphrase.min_character_count was %s; flooring to %d", + minCharacterCount, + min, + ) + } + return CustomRecoveryPassphrase(minCharacterCount = min) +} diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseTest.kt new file mode 100644 index 0000000000..d4e7ac112e --- /dev/null +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseTest.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.wellknown.api + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CustomRecoveryPassphraseTest { + @Test + fun `empty input never satisfies a positive minimum`() { + assertThat(CustomRecoveryPassphrase(minCharacterCount = 1).isSatisfiedBy("")).isFalse() + } + + @Test + fun `input shorter than minimum is not satisfied`() { + assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse() + } + + @Test + fun `input exactly at minimum is satisfied`() { + assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue() + } + + @Test + fun `input longer than minimum is satisfied`() { + assertThat(CustomRecoveryPassphrase(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue() + } +} diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index faad139a36..a64d156f6e 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -6,16 +6,31 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.libraries.wellknown.impl import com.google.common.truth.Truth.assertThat +import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.androidutils.json.DefaultJsonProvider +import io.element.android.libraries.androidutils.json.JsonProvider +import io.element.android.libraries.cachestore.api.CacheData +import io.element.android.libraries.cachestore.api.CacheStore import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore +import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.services.toolbox.api.systemclock.SystemClock +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -36,6 +51,8 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = null, brandColor = null, notificationSound = null, + identityProviderAppScheme = null, + customRecoveryPassphrase = null, ) ) ) @@ -48,13 +65,7 @@ class DefaultSessionWellknownRetrieverTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success( - """{ - "registration_helper_url": "a_registration_url", - "enforce_element_pro": true, - "rageshake_url": "a_rageshake_url", - "brand_color": "#FF0000", - "notification_sound": "a_notification_sound.flac" - }""".trimIndent().toByteArray() + WELLKNOWN_CONTENT.toByteArray() ) } ) @@ -66,6 +77,8 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = "a_rageshake_url", brandColor = "#FF0000", notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", + customRecoveryPassphrase = null, ) ) ) @@ -80,7 +93,8 @@ class DefaultSessionWellknownRetrieverTest { "registration_helper_url": "a_registration_url", "enforce_element_pro": true, "rageshake_url": "a_rageshake_url", - "other": true + // Note the trailing comma, and the comment! + "other": true, }""".trimIndent().toByteArray() ) }, @@ -93,6 +107,94 @@ class DefaultSessionWellknownRetrieverTest { rageshakeUrl = "a_rageshake_url", brandColor = null, notificationSound = null, + identityProviderAppScheme = null, + customRecoveryPassphrase = null, + ) + ) + ) + } + + @Test + fun `get element wellknown with custom recovery passphrase settings`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "custom_recovery_passphrase": { + "min_character_count": 8 + } + }""".trimIndent().toByteArray() + ) + }, + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 8) + ) + ) + ) + } + + @Test + fun `get element wellknown with custom recovery passphrase settings missing min character count floors to 1`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "custom_recovery_passphrase": {} + }""".trimIndent().toByteArray() + ) + }, + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) + ) + ) + ) + } + + @Test + fun `get element wellknown with zero min character count floors to 1`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "custom_recovery_passphrase": { + "min_character_count": 0 + } + }""".trimIndent().toByteArray() + ) + }, + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) + ) + ) + ) + } + + @Test + fun `get element wellknown with negative min character count floors to 1`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "custom_recovery_passphrase": { + "min_character_count": -5 + } + }""".trimIndent().toByteArray() + ) + }, + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1) ) ) ) @@ -123,13 +225,120 @@ class DefaultSessionWellknownRetrieverTest { assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) } - private fun createDefaultSessionWellknownRetriever( + @Test + fun `get element wellknown hitting cache`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { lambdaError() }, + cacheStore = InMemoryCacheStore( + initialData = mapOf( + WELLKNOWN_URL to CacheData( + value = WELLKNOWN_CONTENT, + updatedAt = A_FAKE_TIMESTAMP, + ) + ) + ) + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + brandColor = "#FF0000", + notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", + customRecoveryPassphrase = null, + ) + ) + ) + } + + @Test + fun `get element wellknown hitting cache containing invalid json`() = runTest { + val cacheStore = InMemoryCacheStore( + initialData = mapOf( + WELLKNOWN_URL to CacheData( + value = WELLKNOWN_CONTENT, + updatedAt = A_FAKE_TIMESTAMP, + ) + ) + ) + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success("{}".toByteArray()) + }, + cacheStore = cacheStore, + jsonProvider = JsonProvider { error("Failed to parse JSON") } + ) + assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) + // Ensure that the cache is deleted after the failure to parse it + assertThat(cacheStore.dataMap).isEmpty() + } + + @Test + fun `get element wellknown hitting outdated cache`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success("{}".toByteArray()) + }, + cacheStore = InMemoryCacheStore( + initialData = mapOf( + WELLKNOWN_URL to CacheData( + value = WELLKNOWN_CONTENT, + updatedAt = 0L, + ) + ), + ), + // 3 days later, so the cache is outdated + systemClock = FakeSystemClock(3 * 24 * 60 * 60 * 1000L) + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + brandColor = "#FF0000", + notificationSound = "a_notification_sound.flac", + identityProviderAppScheme = "an_app_scheme", + customRecoveryPassphrase = null, + ) + ) + ) + // Next call returns the updated value + runCurrent() + assertThat(sut.getElementWellKnown()).isEqualTo( + WellknownRetrieverResult.Success( + anElementWellKnown() + ) + ) + } + + private fun TestScope.createDefaultSessionWellknownRetriever( getUrlLambda: (String) -> Result, + jsonProvider: JsonProvider = DefaultJsonProvider(), + cacheStore: CacheStore = InMemoryCacheStore(), + systemClock: SystemClock = FakeSystemClock(), ) = DefaultSessionWellknownRetriever( matrixClient = FakeMatrixClient( userIdServerNameLambda = { "user.domain.org" }, getUrlLambda = getUrlLambda, ), - json = DefaultJsonProvider(), + json = jsonProvider, + cacheStore = cacheStore, + systemClock = systemClock, + sessionCoroutineScope = backgroundScope, ) + + companion object { + private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json" + private const val WELLKNOWN_CONTENT = """{ + "registration_helper_url": "a_registration_url", + "enforce_element_pro": true, + "rageshake_url": "a_rageshake_url", + "brand_color": "#FF0000", + "notification_sound": "a_notification_sound.flac", + "idp_app_scheme": "an_app_scheme" + }""" + } } diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt index 7457aafc98..51b06f7226 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt @@ -8,6 +8,7 @@ package io.element.android.features.wellknown.test +import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase import io.element.android.libraries.wellknown.api.ElementWellKnown fun anElementWellKnown( @@ -16,10 +17,20 @@ fun anElementWellKnown( rageshakeUrl: String? = null, brandColor: String? = null, notificationSound: String? = null, + identityProviderAppScheme: String? = null, + customRecoveryPassphrase: CustomRecoveryPassphrase? = null, ) = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, rageshakeUrl = rageshakeUrl, brandColor = brandColor, notificationSound = notificationSound, + identityProviderAppScheme = identityProviderAppScheme, + customRecoveryPassphrase = customRecoveryPassphrase, +) + +fun aCustomRecoveryPassphrase( + minCharacterCount: Int = 8, +) = CustomRecoveryPassphrase( + minCharacterCount = minCharacterCount, ) diff --git a/libraries/workmanager/api/build.gradle.kts b/libraries/workmanager/api/build.gradle.kts index b53ed40394..238dc57664 100644 --- a/libraries/workmanager/api/build.gradle.kts +++ b/libraries/workmanager/api/build.gradle.kts @@ -15,6 +15,6 @@ android { dependencies { api(libs.androidx.workmanager.runtime) - + implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) } diff --git a/libraries/workmanager/impl/build.gradle.kts b/libraries/workmanager/impl/build.gradle.kts index 878edb6fe2..c1874bfa74 100644 --- a/libraries/workmanager/impl/build.gradle.kts +++ b/libraries/workmanager/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.di) + implementation(projects.libraries.sessionStorage.api) testCommonDependencies(libs, false) testImplementation(projects.libraries.sessionStorage.test) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index d6a2cbea3d..ccf39b9c41 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -39,7 +39,7 @@ 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 = 4 +private const val versionMonth = 6 /** * Release number in the month. Value must be in [0,99]. @@ -61,19 +61,19 @@ object Versions { * Compile SDK version. Must be updated when a new Android version is released. * When updating COMPILE_SDK, please also update BUILD_TOOLS_VERSION. */ - const val COMPILE_SDK = 36 + const val COMPILE_SDK = 37 /** * Build tools version. Must be kept in sync with COMPILE_SDK. * The value is used by the release script. */ @Suppress("unused") - private const val BUILD_TOOLS_VERSION = "36.0.0" + private const val BUILD_TOOLS_VERSION = "37.0.0" /** * Target SDK version. Should be kept up to date with COMPILE_SDK. */ - const val TARGET_SDK = 36 + const val TARGET_SDK = 37 /** * Minimum SDK version for FOSS builds. diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index 5f243f4319..1f24473f8e 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -9,67 +9,111 @@ package extension import Versions -import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.ApplicationDefaultConfig +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CompileOptions +import com.android.build.api.dsl.LibraryDefaultConfig +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.dsl.Lint import isEnterpriseBuild import org.gradle.api.Project import java.io.File -fun CommonExtension<*, *, *, *, *, *>.androidConfig(project: Project) { - defaultConfig { - compileSdk = Versions.COMPILE_SDK - minSdk = Versions.minSdk - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" +fun ApplicationExtension.androidAppConfig(project: Project) { + compileSdk = Versions.COMPILE_SDK - vectorDrawables { - useSupportLibrary = true - generatedDensities() - } - } - - compileOptions { - sourceCompatibility = Versions.javaVersion - targetCompatibility = Versions.javaVersion - } - - testOptions { - unitTests.isReturnDefaultValues = true - } + defaultConfig(::defaultApplicationConfig) + compileOptions(::defaultCompileOptions) + testOptions(::defaultTestOptions) lint { - lintConfig = File("${project.rootDir}/tools/lint/lint.xml") - if (isEnterpriseBuild) { - // Disable check on ObsoleteSdkInt for Enterprise builds - // since the min sdk is higher for Enterprise builds - disable.add("ObsoleteSdkInt") - } - checkDependencies = false - abortOnError = true - ignoreTestSources = true - ignoreTestFixturesSources = true - checkGeneratedSources = false + project.defaultLintOptions(this) } } -fun CommonExtension<*, *, *, *, *, *>.composeConfig() { +fun LibraryExtension.androidLibraryConfig(project: Project) { + compileSdk = Versions.COMPILE_SDK - buildFeatures { - compose = true - } - - packaging { - resources.excludes.apply { - add("META-INF/AL2.0") - add("META-INF/LGPL2.1") - } - } + defaultConfig(::defaultLibraryConfig) + compileOptions(::defaultCompileOptions) + testOptions(::defaultTestOptions) lint { - // Extra rules for compose - // Disabled until lint stops inspecting generated ksp files... - // error.add("ComposableLambdaParameterNaming") - error.add("ComposableLambdaParameterPosition") - ignoreTestFixturesSources = true - checkGeneratedSources = false + project.defaultLintOptions(this) } } +fun ApplicationExtension.composeAppConfig() { + buildFeatures(::defaultComposeBuildFeatures) + packaging(::defaultPackagingOptions) + lint(::defaultComposeLintOptions) +} + +fun LibraryExtension.composeLibraryConfig() { + buildFeatures(::defaultComposeBuildFeatures) + packaging(::defaultPackagingOptions) + lint(::defaultComposeLintOptions) +} + +fun defaultApplicationConfig(applicationDefaultConfig: ApplicationDefaultConfig) = applicationDefaultConfig.apply { + minSdk = Versions.minSdk + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + vectorDrawables { + useSupportLibrary = true + generatedDensities() + } +} + +fun defaultLibraryConfig(libraryDefaultConfig: LibraryDefaultConfig) = libraryDefaultConfig.apply { + minSdk = Versions.minSdk + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + vectorDrawables { + useSupportLibrary = true + generatedDensities() + } +} + +fun defaultCompileOptions(compileOptions: CompileOptions) = compileOptions.apply { + sourceCompatibility = Versions.javaVersion + targetCompatibility = Versions.javaVersion +} + +fun defaultTestOptions(testOptions: com.android.build.api.dsl.TestOptions) = testOptions.apply { + unitTests.isReturnDefaultValues = true +} + +fun defaultComposeBuildFeatures(buildFeatures: com.android.build.api.dsl.BuildFeatures) = buildFeatures.apply { + compose = true +} + +fun defaultPackagingOptions(packagingOptions: com.android.build.api.dsl.Packaging) = packagingOptions.apply { + resources.excludes.apply { + add("META-INF/AL2.0") + add("META-INF/LGPL2.1") + } +} + +fun defaultComposeLintOptions(lint: Lint) = lint.apply { + // Extra rules for compose + // Disabled until lint stops inspecting generated ksp files... + // error.add("ComposableLambdaParameterNaming") + error.add("ComposableLambdaParameterPosition") + ignoreTestFixturesSources = true + checkGeneratedSources = false +} + +fun Project.defaultLintOptions(lint: Lint) = lint.apply { + lintConfig = File("${project.rootDir}/tools/lint/lint.xml") + if (isEnterpriseBuild) { + // Disable check on ObsoleteSdkInt for Enterprise builds + // since the min sdk is higher for Enterprise builds + disable.add("ObsoleteSdkInt") + } + checkDependencies = false + abortOnError = true + ignoreTestSources = true + ignoreTestFixturesSources = true + checkGeneratedSources = false +} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index ce5c324ff4..3e01ffc25f 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -104,6 +104,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) + implementation(project(":libraries:cachestore:impl")) implementation(project(":libraries:session-storage:impl")) implementation(project(":libraries:mediapickers:impl")) implementation(project(":libraries:mediaupload:impl")) @@ -120,7 +121,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:troubleshoot:impl")) implementation(project(":libraries:fullscreenintent:impl")) implementation(project(":libraries:wellknown:impl")) - implementation(project(":libraries:oidc:impl")) + implementation(project(":libraries:oauth:impl")) implementation(project(":libraries:workmanager:impl")) implementation(project(":libraries:recentemojis:impl")) } diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 5d6b1ddabb..c851a25ab9 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -126,6 +126,14 @@ fun Project.setupKover() { "io.element.android.tests.konsist.failures", // Copied from Appyx "io.element.android.libraries.architecture.appyx.SafeChildrenTransitionScope", + // DI-generated classes + "io.element.android.x.di.*Impl", + "io.element.android.x.di.*Impls", + "io.element.android.x.di.*Mirror", + "io.element.android.x.di.*Factory", + $$"io.element.android.*$Metro*", + $$"io.element.android.*$Factory*", + $$"io.element.android.*$Impl*", ) annotatedBy( "androidx.compose.ui.tooling.preview.Preview", diff --git a/plugins/src/main/kotlin/extension/locales.kt b/plugins/src/main/kotlin/extension/locales.kt index 650fbc4d72..2774b078b5 100644 --- a/plugins/src/main/kotlin/extension/locales.kt +++ b/plugins/src/main/kotlin/extension/locales.kt @@ -5,6 +5,7 @@ package extension val locales = setOf( "be", "bg", + "ca", "cs", "cy", "da", diff --git a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts index a489884df5..4683ee1b5f 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts @@ -9,9 +9,9 @@ /** * This will generate the plugin "io.element.android-compose-application" to use by app */ -import extension.androidConfig +import extension.androidAppConfig import extension.commonDependencies -import extension.composeConfig +import extension.composeAppConfig import extension.composeDependencies import extension.setupKover import org.gradle.accessors.dm.LibrariesForLibs @@ -19,14 +19,13 @@ import org.gradle.accessors.dm.LibrariesForLibs val libs = the() plugins { id("com.android.application") - id("kotlin-android") id("com.autonomousapps.dependency-analysis") id("org.jetbrains.kotlin.plugin.compose") } android { - androidConfig(project) - composeConfig() + androidAppConfig(project) + composeAppConfig() compileOptions { isCoreLibraryDesugaringEnabled = true } diff --git a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts index 26b1a1f122..1fd697806d 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts @@ -9,9 +9,9 @@ /** * This will generate the plugin "io.element.android-compose-library", used in android library with compose modules. */ -import extension.androidConfig +import extension.androidLibraryConfig import extension.commonDependencies -import extension.composeConfig +import extension.composeLibraryConfig import extension.composeDependencies import extension.setupKover import org.gradle.accessors.dm.LibrariesForLibs @@ -19,14 +19,13 @@ import org.gradle.accessors.dm.LibrariesForLibs val libs = the() plugins { id("com.android.library") - id("kotlin-android") id("com.autonomousapps.dependency-analysis") id("org.jetbrains.kotlin.plugin.compose") } android { - androidConfig(project) - composeConfig() + androidLibraryConfig(project) + composeLibraryConfig() compileOptions { isCoreLibraryDesugaringEnabled = true } diff --git a/plugins/src/main/kotlin/io.element.android-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-library.gradle.kts index c10c1bb3d7..18b1e1d3f8 100644 --- a/plugins/src/main/kotlin/io.element.android-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-library.gradle.kts @@ -9,7 +9,7 @@ /** * This will generate the plugin "io.element.android-library", used in android library without compose modules. */ -import extension.androidConfig +import extension.androidLibraryConfig import extension.commonDependencies import extension.setupKover import org.gradle.accessors.dm.LibrariesForLibs @@ -17,12 +17,11 @@ import org.gradle.accessors.dm.LibrariesForLibs val libs = the() plugins { id("com.android.library") - id("kotlin-android") id("com.autonomousapps.dependency-analysis") } android { - androidConfig(project) + androidLibraryConfig(project) compileOptions { isCoreLibraryDesugaringEnabled = true } diff --git a/screenshots/de/appnav.root_RootView_Day_0_de.png b/screenshots/de/appnav.root_RootView_Day_0_de.png index 7f70b90751..2d0d9fab03 100644 --- a/screenshots/de/appnav.root_RootView_Day_0_de.png +++ b/screenshots/de/appnav.root_RootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa11b2165af4a12cd518fb3f9a06b91d9ce87b468705efc7a962bf34a8278fac -size 26284 +oid sha256:c3082552bf96131ddba157697bb254e22d6277537759c32dfabb56a2e99a9138 +size 27116 diff --git a/screenshots/de/appnav.root_RootView_Day_1_de.png b/screenshots/de/appnav.root_RootView_Day_1_de.png index e1796d211b..6d1c994b27 100644 --- a/screenshots/de/appnav.root_RootView_Day_1_de.png +++ b/screenshots/de/appnav.root_RootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35a9ac561c2546fd94121cb0c80e1f5be1fe147f425be4d087b647ae0f204afd -size 29986 +oid sha256:f9231272736124f1e54a3422310d2dd5c7ad81e850548886062df61de36165c4 +size 30868 diff --git a/screenshots/de/features.call.impl.ui_CallScreenView_Day_3_de.png b/screenshots/de/features.call.impl.ui_CallScreenView_Day_3_de.png index bfa7251296..0996d18bf0 100644 --- a/screenshots/de/features.call.impl.ui_CallScreenView_Day_3_de.png +++ b/screenshots/de/features.call.impl.ui_CallScreenView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5988ca59d9fcfaccbf4bc2929d7c135f67be0852e739caeb566b0518fac7f75 -size 19021 +oid sha256:bc9627a6288987e446206dc54abb8e1319ba6996674f0b588decd5418eeafc1b +size 18688 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 8843434a9f..08a4e04bba 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:e31bebf55244a497eaf3442701b0db8b5ee6c061944c1b4e72da487660104643 -size 40044 +oid sha256:9b6138083429a82d1de576009df367a52361b39e5ac094d0c4abc0bb47e89550 +size 41888 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 4e4cdee57b..5771464dd2 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:9ce67e2cd809b3ba7f32c57cf87208e71a92df71b545bea2554ed7f6d28e78e7 -size 41381 +oid sha256:981e3075d5285a630932c02427cca3d46efad74a640f12d35da8e0f29fb4b779 +size 43474 diff --git a/screenshots/de/features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_de.png b/screenshots/de/features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_de.png index 21c6405a77..1cc92bddf4 100644 --- a/screenshots/de/features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_de.png +++ b/screenshots/de/features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22e0fb8867a616d7d575f9262996f66730cb61f8b0e9c2c74fffbb92b3a1fd06 -size 25852 +oid sha256:ac79f05488afb65d5675a8068ac1c70c7cddc81b1db5445f1314ce53a1da8ba1 +size 27143 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png index fdbc13a1ee..5edce6541b 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b62cf320453eb36be6e486e3100c3be1420fcdfd068f2276d27a824fb84e3f05 -size 42064 +oid sha256:9c1602858c58d4f05c5c3d816cde905af8cd888ae9337215119902b0dbdf1cbc +size 43609 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png index ddbdfb051f..641671bf3f 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88633b6deed6e44e354cf3c8ac9e8553f0dd41743198cf5d8ff57de51ecd45e4 -size 35217 +oid sha256:1acc89193300b0ea268c127c98c7d46b8e23841933750d99d597a8713e1698ed +size 36762 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png index 0dafa02950..b9ce42a450 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0887222f914448055346fd314b3a8bfdaeda5262b76f509749ec01877728a821 -size 48344 +oid sha256:0f5bddbb812e5fd0863a8c1308349f627f0e8fd865ee36f995a29666e0816b0d +size 49843 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png index 8051720f82..f8be736047 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c355fb7f9e899e9cc7fe9a4a0aefacb7f57e669b60927b9c77f29630a6168b0 -size 41774 +oid sha256:5e141bd8ca4767ded168a1e7cb574df8d33797e05ced6615287469b282d985b1 +size 43319 diff --git a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_de.png b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_de.png index 40197c311f..5697209279 100644 --- a/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_de.png +++ b/screenshots/de/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6085e75ee0b5669b078b7ff40c6f9e91f04263967a46076dda95802992224318 -size 31603 +oid sha256:15bba5407bbda9070cf2109165e4fb9455450c8d6dd504dea7b521640fcfa48d +size 33229 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png new file mode 100644 index 0000000000..3419248cbe --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9af82b0e785a85acb47d0b02b682c659bc13dbff01eb969f367d2ee413b6cbb1 +size 25165 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png new file mode 100644 index 0000000000..706b96c796 --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37dc07b7be3f81a8b3925e1325baecff04dc2f3cd92316140f969610b6362169 +size 25691 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png new file mode 100644 index 0000000000..b8c5829e1e --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListContextMenu_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ace88630fc2d12ce8f9f610683626ff9d8ad8d3a18564791d3abdcff5743ed46 +size 27731 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_de.png b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_de.png deleted file mode 100644 index b51bbdbfc7..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf13adc113fe7b1127eedde7ed659ac11afb25b25be22f757dfd22c363e011f5 -size 29190 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_de.png b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_de.png new file mode 100644 index 0000000000..67b0361c5c --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a5736c3ce48028bde69a7f029ffaed5d18e490b32fbb455d35d89f90f7e169 +size 30385 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_de.png b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_de.png new file mode 100644 index 0000000000..c39d87d73c --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9caedf891866305599b0cc1887f46fc2ae7e0c6ca903860df247eb48eadf3708 +size 45796 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_de.png b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_de.png new file mode 100644 index 0000000000..950a33e731 --- /dev/null +++ b/screenshots/de/features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:469c3186032dae6ed8d4203277b9a45d5c5f648e7688a5a6804231ac648daef4 +size 30940 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_de.png b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_de.png deleted file mode 100644 index 097191f4f2..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba628bba3efe736cbfd164d8c5a403ef042cefd29fef6307fc2c2694798b0e18 -size 23407 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_de.png b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_de.png deleted file mode 100644 index d9912fb3e0..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:811bc09258019248f5cd94ee8cfeb070b1471f510ba4ad16b427e4c574db46a9 -size 23660 diff --git a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_de.png b/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_de.png deleted file mode 100644 index cc6b3c7a57..0000000000 --- a/screenshots/de/features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7dada5eb605d5558baea8b01101bfbb8cc31338b2ce76035a8fcb449cdd8e77b -size 25776 diff --git a/screenshots/de/features.home.impl_HomeView_Day_6_de.png b/screenshots/de/features.home.impl_HomeView_Day_6_de.png index 467350fe56..10abd6aef3 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_6_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cc8182e0e5413cc51f0df6d2724efae8af4101f9ac6cf21ca9260c2a2aa245a -size 57989 +oid sha256:a50cbaec05c9547c992342aba6f5042bbd0dcb6cae0a599e91b26edbafb59ac0 +size 58277 diff --git a/screenshots/de/features.home.impl_HomeView_Day_7_de.png b/screenshots/de/features.home.impl_HomeView_Day_7_de.png index 774818bd57..2e95e34feb 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_7_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f235995960c6237f4e03e7b0736cb280e1627590b0761a334c7a969491a12da8 -size 57321 +oid sha256:b53cae15b14893fce996dc087b4907cb21104e220d143e64192753d1088c0c44 +size 57599 diff --git a/screenshots/de/features.home.impl_HomeView_Day_8_de.png b/screenshots/de/features.home.impl_HomeView_Day_8_de.png index e644082cae..5f62cc2ebf 100644 --- a/screenshots/de/features.home.impl_HomeView_Day_8_de.png +++ b/screenshots/de/features.home.impl_HomeView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba1fbfc866d6de2cafae55b6da0721f12a1acb25163ad31ae2995f6ff9016856 -size 56039 +oid sha256:d88ef28257f6c0893e8bfff2d98e5aff52f0534539e91fe6ca490989548a65b9 +size 56346 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png index 79a282bf32..5fb0408a49 100644 --- a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:141bf23853b7ba97cffd4d54e9e0a48843d0a49e7f6d6a91c41f110d2df1c7dd -size 24180 +oid sha256:d20416987d2485431b66ebb29af1f03efa2e04da7ef5875eda62d7c5e187052b +size 24828 diff --git a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png index bc89b471f2..4aa0388c8a 100644 --- a/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png +++ b/screenshots/de/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a39d0a1c752533bfb097ad1d2c59978123545ee76259e26efe6e3fbae209db69 -size 31441 +oid sha256:62d531d6b01266019e2fa686321e93d64bcffd5b1786f7ad4efac83fcc2a2fc5 +size 32141 diff --git a/screenshots/de/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_de.png b/screenshots/de/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_de.png new file mode 100644 index 0000000000..630aee04c5 --- /dev/null +++ b/screenshots/de/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:495afd76bb17398747975e89e5dadb28e7b8c20309725264be293420b8518ec8 +size 38456 diff --git a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_2_de.png b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_2_de.png index 92ffd71139..308abe5a72 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_2_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf9aea8bd7c9f6d7eb7f31a71ec412ae0f143c60bc6834a1f2ad99e538d87875 -size 33758 +oid sha256:a1e6cd137958c6f3cd9ddad7cdf9dcd35c9b9963cdab164c232ebefa50b7a81b +size 33815 diff --git a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_3_de.png b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_3_de.png index 0f07d0568c..fbcb03812f 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_3_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:968c091419c902a4c135a4498ebe2422566d0f2dac72882e81ac5dc6572a9e77 -size 42859 +oid sha256:026cde07bd625e7457af44ef19c4865f6bad8fc7c8ddfcc43c7a6dbdd4acf467 +size 33651 diff --git a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_4_de.png b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_4_de.png index feacf82d89..0b411260ec 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_4_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e71f2781efd689a0c57613cf03fec734474128ae444b8079d6124f8b9aa19fb -size 45605 +oid sha256:0b41b171588b46f2b17fe366d311da43653c43dc1eccf539c7f02c12d1c7a21f +size 45688 diff --git a/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_8_de.png b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_8_de.png new file mode 100644 index 0000000000..cba542a991 --- /dev/null +++ b/screenshots/de/features.linknewdevice.impl.screens.error_ErrorView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e77e5930c00c3389179fc5a60a6a2d173223adaf742a53b6900ab96ab0af88a +size 22202 diff --git a/screenshots/de/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_de.png b/screenshots/de/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_de.png new file mode 100644 index 0000000000..f79c47c87a --- /dev/null +++ b/screenshots/de/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8dcfd0a6965522ddc3d1a0571176eb6889bbf15a390ea7d4348f002ec9696d5 +size 33189 diff --git a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_de.png b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_de.png index 1ea459175f..0e9606c453 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91215fa6820bbe0d6da772de2720cde77bc731f28954d067c6a6a0d95709f624 -size 19238 +oid sha256:748da581859b8b1a05a2b5703c17b31f6725d26713053063dc825b425b4d4131 +size 19317 diff --git a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_de.png b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_de.png index 45ed4366dc..7127f05f27 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:419f4edd8b097ca40f68d5012c5b0c0e6c1301b6efcd01b13ce0c8e7a13996c6 -size 25430 +oid sha256:446b2dbace2a10eb7332886ce7fa83f992c5314f3a66fd402a13200e9f0f06b2 +size 25489 diff --git a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_de.png b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_de.png index a9af23935f..c5645b1b44 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e40215a3f06166fa78319875ca074876e41e19a9155794f4d0b68a132045a276 -size 27975 +oid sha256:84da7365709446471e67dedd9f733786989f56fa1d3fe83ea5ec02344076983f +size 28966 diff --git a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_de.png b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_de.png index e172ea6f3d..f466ac0135 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1684330a9b215bb22fbefe2e59d456c9cbc31e75e01828c67a256b0a02b667f -size 25956 +oid sha256:a7b92adb8584cf31853a01bda52aacab97df832360399adbf63980ebb612ce76 +size 26029 diff --git a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_de.png b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_de.png index 4b15ed217c..2fdab6284a 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e96f99f81040aa3214a1acd937d25299d080ca0191258562cb08f78fbdeff7e -size 26188 +oid sha256:cde54cd972be78cb89278b4382d7b71297bdeddb0a16503211ed0342ad059a2b +size 26237 diff --git a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_de.png b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_de.png index 75f85cba2e..fbda7d633d 100644 --- a/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_de.png +++ b/screenshots/de/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:759da98dca2f26b5a7e87954fceba866e7fb526dc36587b62cb3d01f3ba29b53 -size 34992 +oid sha256:76c3bacabeab51017a076a58ff61176ec45fff9711b86471cf1e6752692a201a +size 35056 diff --git a/screenshots/de/features.location.api_LiveLocationSharingBanner_Day_0_de.png b/screenshots/de/features.location.api_LiveLocationSharingBanner_Day_0_de.png new file mode 100644 index 0000000000..f889a65156 --- /dev/null +++ b/screenshots/de/features.location.api_LiveLocationSharingBanner_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efd6cb8498b29f79769b3287a062ee1ddad3c34195aa5d653b8e28e4e6f973bc +size 10767 diff --git a/screenshots/de/features.location.api_RenderingMapsNotSupportedDialog_Day_0_de.png b/screenshots/de/features.location.api_RenderingMapsNotSupportedDialog_Day_0_de.png new file mode 100644 index 0000000000..06c4be2c81 --- /dev/null +++ b/screenshots/de/features.location.api_RenderingMapsNotSupportedDialog_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3065603f9b067f0ddc89a7b8dc24921afd031f4658f6ac11de8feb0c4bf26b62 +size 23910 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 index f4290dda6e..8381a894f4 100644 --- a/screenshots/de/features.location.impl.share_ShareLocationView_Day_3_de.png +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2eb237f3bea51645310fe28e66a374ee7eea722d262261faf7a2d4ad7bfc9515 -size 30400 +oid sha256:6ad541415245d6e6ca95514db2aa053691d8d2c8193cf0268be67cc74b1af4ea +size 32928 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 index bf8a621fe1..bb93cfc575 100644 --- a/screenshots/de/features.location.impl.share_ShareLocationView_Day_6_de.png +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a5606179cc7dacc1d0bc86ff0f38a7654cebeb7c428575e2da51d4af0ba12b9 -size 43384 +oid sha256:1342bc5c63e6fa38797d1867b5006ebc91cfa926d189e4ff9060f84efd8146ca +size 26114 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_7_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_7_de.png new file mode 100644 index 0000000000..0663eceb1d --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b08870ebcd6da6372363b0af0334524a4288db7242cdb5a377512ef9aa728a66 +size 38883 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_8_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_8_de.png new file mode 100644 index 0000000000..bf8a621fe1 --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a5606179cc7dacc1d0bc86ff0f38a7654cebeb7c428575e2da51d4af0ba12b9 +size 43384 diff --git a/screenshots/de/features.location.impl.share_ShareLocationView_Day_9_de.png b/screenshots/de/features.location.impl.share_ShareLocationView_Day_9_de.png new file mode 100644 index 0000000000..d8b464213f --- /dev/null +++ b/screenshots/de/features.location.impl.share_ShareLocationView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1ab182dfc2b45c80972722b1963a75c29175c1d09369fafb96c7b0ea04bf08a +size 21171 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 8cf129ffae..6a87af5d60 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:2ad009477e6df04362db43d17c2a43599ae19812ef4244b0ffdd436e18662969 -size 32383 +oid sha256:da8a5e9159c4817b5c500d721e378e51d1f3ed6bea5468b54e4e66db1969c292 +size 34873 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 index d52980f113..0c68797fef 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e8cb1edecba35913251d57d6eaa9d48149f7b2a26c180978c0ae34d3d354d97 -size 19693 +oid sha256:6fe56f20ce5792f9dd60bf0ae08fa5c5c78fbb7a5f1a80eb2fe973716be69c00 +size 19831 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 index 0c68797fef..a49023fb61 100644 --- a/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png +++ b/screenshots/de/features.location.impl.show_ShowLocationView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fe56f20ce5792f9dd60bf0ae08fa5c5c78fbb7a5f1a80eb2fe973716be69c00 -size 19831 +oid sha256:fa435ee5d684b82c66c7207e0bcdc2d608a634e02d534530cccf8abd0f40f784 +size 21292 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_de.png new file mode 100644 index 0000000000..048facf45b --- /dev/null +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b4ead892d27f2eb5fd7c9319e3c954ab2ae57c68180e08b30c07741c53cadda +size 35383 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_de.png new file mode 100644 index 0000000000..af1fe3af55 --- /dev/null +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b481b2bafe288542d25c301aa568fdbd9d63170b1c59b01fdacacc69d0395b3 +size 17352 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png index 44b295fbb3..bde4a6cba7 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c0f9f0948da8553e64cbd3dd8643420dbbe0b2ee0655ef09b4d4869495f8f67 -size 38036 +oid sha256:4c764d8bd6dbf39c937b45eaa6a24b048ab9db1528494503494c02b6fcdf1523 +size 38035 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png index 3b324af20f..2ec1d65aaf 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53f201aeace81ee0564d41542bfc8ea904d71527746ead6cbd6e9faddf29e699 -size 38421 +oid sha256:b047b01f0573eff0ae7798afbbed1df7e52c754df8d5a7744958eed939179443 +size 38422 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png index 0343205abb..b6dd30b9a3 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15200b3f62fbd9b0ecab51560faa4696f384daf7463a407445b2191f94c13645 -size 38695 +oid sha256:962d90cd71c5ed7ea19ad08bb641f197466defb85840ff7ed30d49656eee1b06 +size 38679 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_3_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_3_de.png index d78926b966..06011700ab 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_3_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb3267c007e01c5e691841911e4aa7bcfe319077a6b4f49d945e9bacc5474087 -size 45041 +oid sha256:ca9d69de30e7fa7008263c78f5586814d109ab8c043a19ed687f9327b955e03e +size 45033 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png index 2b7542cc53..ba059493a7 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30358430cd572face46c5d6e060fe0b93947670aa4c4caa091f8a50521ae36a7 -size 35467 +oid sha256:902411746e402c8c5e82393ba854d40a17f60b0ff5ab13ce3b8928449bf92fa4 +size 35471 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_5_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_5_de.png index 4502875e08..e9d66d7ee5 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_5_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71f8a9d484a4056507b129f7fb390b2c67c99ec46a928da15caac4f95ffbd6fd -size 42006 +oid sha256:41a80abb68a227320c18a797f73efe0fe9dc0ebed50fe8f6523c433fc566e6df +size 41989 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_6_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_6_de.png index 014fdcd4b7..759b17db50 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_6_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c2e7d3a0b31388c8f373920e71ca23228fd5961a1705b3032b1407f35dc2e28 -size 31051 +oid sha256:93caba67729429e85bfe4a26846a1ea1c122f1b1aa7e0622dc11bdecb2741cd8 +size 31032 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png index 2840a40c3b..1fed050403 100644 --- a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91a83c3de10fea2e5edf3d96bc9ecc1fd81000ec957970696bca6c13d4d9dca3 -size 31820 +oid sha256:fc5e9086a7896b21d7a46fbad3ee5962be125fdd5f9609603dfe6f1779395c7a +size 31798 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_8_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_8_de.png new file mode 100644 index 0000000000..2e062dad91 --- /dev/null +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_8_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3cbbc047bbdfa813c4bb984a469370b33d8ceb44b1e57e2a40cf84c7b20f7a1 +size 41492 diff --git a/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_9_de.png b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_9_de.png new file mode 100644 index 0000000000..9968deee8e --- /dev/null +++ b/screenshots/de/features.lockscreen.impl.unlock_PinUnlockView_Day_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcfcf26c1c2cb4e33e6709232f535331ce00fdde22455940e7b02b4dc727ecb2 +size 32938 diff --git a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_5_de.png b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_5_de.png index c6a8d72619..ec9de6ee4e 100644 --- a/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_5_de.png +++ b/screenshots/de/features.login.impl.changeserver_ChangeServerView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c3b505df79be2829a102e9a0249fc23c5f5de1e7fa19570a1fd1e5ab7f9aaee -size 30819 +oid sha256:228c78854193cffdf1a0a22a80eb742b01dcef195192196f7b63c32f3d0666e8 +size 30823 diff --git a/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png b/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png index c6a8d72619..ec9de6ee4e 100644 --- a/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png +++ b/screenshots/de/features.login.impl.login_LoginModeView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c3b505df79be2829a102e9a0249fc23c5f5de1e7fa19570a1fd1e5ab7f9aaee -size 30819 +oid sha256:228c78854193cffdf1a0a22a80eb742b01dcef195192196f7b63c32f3d0666e8 +size 30823 diff --git a/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_de.png b/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_de.png index 50663c9d70..7848b3de7f 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8153b9a20bb2cda60b59abb617f527b165f54fee15235384b580de74b2ae31aa -size 37912 +oid sha256:7ead8637651c4d0e3c0f6e78c596c4781d00ecb493bdf54ac96670b55ff541c2 +size 38217 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_de.png index 92ffd71139..308abe5a72 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf9aea8bd7c9f6d7eb7f31a71ec412ae0f143c60bc6834a1f2ad99e538d87875 -size 33758 +oid sha256:a1e6cd137958c6f3cd9ddad7cdf9dcd35c9b9963cdab164c232ebefa50b7a81b +size 33815 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_de.png index 3e1a99aec0..da94eb9d22 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de35cb70fa0076e5c1d0ab35cdae0b48cac90aa0c0223538276054e69768fd2a -size 42776 +oid sha256:3eeb6eb4ff66c0558d2abc960a8bffc757d6a8fd043f9c4eee4667176de38617 +size 43485 diff --git a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_de.png b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_de.png index 11011887f3..49aa77efd2 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a4508363f01ad58b30503ff92236349b619feaa5716524438a2f52f033d71e6 -size 26225 +oid sha256:e1c0c74a437c19c02f04c6de9898fc8fcce98ebdd0161f97d9973d6bf091db58 +size 27056 diff --git a/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_de.png b/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_de.png index e55f0530fd..4119ae071b 100644 --- a/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_de.png +++ b/screenshots/de/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27bdc5b6fc21700a4f8512230084f1131d9804eb7cd4366d61f90deaf55bb60 -size 56835 +oid sha256:b742d5311fc1961bc9a5a61658e77fdf1ab3f83b5fb93e0a4fedd7f50a3faacb +size 56999 diff --git a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_0_de.png b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_0_de.png index 38bcc092a9..c19097e89a 100644 --- a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_0_de.png +++ b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21e1cce3efb6c835a83b1a0c29dc1aadbcf6d5a4c2e839343f71fa30ea6dd0c4 -size 91151 +oid sha256:392e090f14a98249b1cf3cf4acf180a817c41dbc8b4320ad5b8a2b4636ba0f14 +size 86883 diff --git a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_1_de.png b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_1_de.png index 4fe8b578bd..c84f36df47 100644 --- a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_1_de.png +++ b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be7af996c56f6c75636688d88c70814dd7c93e3963255d6dc2e89fc8032a51c5 -size 90886 +oid sha256:2488a2d453b8a8cd086e531e1c49ac2d41186713b85c1aba3df73cf6c7a5f1e8 +size 86605 diff --git a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_2_de.png b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_2_de.png index e22e792f54..8dd9a8233d 100644 --- a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_2_de.png +++ b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7901b1eb373bfac3fa7a9549c66c157b0d70cd885364646fa37e014056d0bcd8 -size 78805 +oid sha256:f0870cc34ef448fc914f7f4a688781bacaca59edad593509ee5cb333fdcf4acd +size 73713 diff --git a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_3_de.png b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_3_de.png index c872795d91..50752acb56 100644 --- a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_3_de.png +++ b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40745997037a4b3856628bc8d485c8131f4586e0f855513fdfe97b2470507bc4 -size 70087 +oid sha256:844f24409611ccc30b514427c72cd057ed838a1b4ba058a4c6dd941858908e65 +size 66529 diff --git a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_4_de.png b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_4_de.png index 198287f424..e20e0c63c8 100644 --- a/screenshots/de/features.logout.impl_AccountDeactivationView_Day_4_de.png +++ b/screenshots/de/features.logout.impl_AccountDeactivationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3471dcaa2c715b6c3fb8c6b5223f1cb398af8d9b7ff013af11f4c6feef7d4615 -size 61333 +oid sha256:cf40d60799deb62643ca650deb1a5b5e18f5d26ebca7927064c51a0f3a8b771a +size 57890 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_1_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_1_de.png index ba6edcd46e..7354b5bf41 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_1_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f86f77e8055c5d568e4f1e3bf1757a140fef5839eadd8f17731fc079a0c08bbf -size 58945 +oid sha256:08fa6f2925434cc451adb7296041af54941b7b9e126887bd529692fa208060ae +size 69544 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_3_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_3_de.png index ba6edcd46e..7354b5bf41 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_3_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f86f77e8055c5d568e4f1e3bf1757a140fef5839eadd8f17731fc079a0c08bbf -size 58945 +oid sha256:08fa6f2925434cc451adb7296041af54941b7b9e126887bd529692fa208060ae +size 69544 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_7_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_7_de.png index 7704df7d91..ed1172e6ae 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_7_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a56b1ee1bde9ad25090371050081ce13c900d6d085c21b63abd858229fe482d -size 42706 +oid sha256:fadcc1a9018cca35bd08be7f7f4d670e55ec362c15c0ef89625988da6be95ca9 +size 47451 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_8_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_8_de.png index 71073f21cd..dd9ecc5fed 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_8_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed9b2497d9c154f8bad06b5f0405d193f21549d9a0b4a661becce745d7203002 -size 50459 +oid sha256:652613c8509079b395d964c0290272a09b6a9bf4ca02c18d6edcae0b43e06d16 +size 61053 diff --git a/screenshots/de/features.logout.impl_LogoutView_Day_9_de.png b/screenshots/de/features.logout.impl_LogoutView_Day_9_de.png index dccb62e40c..dd9ecc5fed 100644 --- a/screenshots/de/features.logout.impl_LogoutView_Day_9_de.png +++ b/screenshots/de/features.logout.impl_LogoutView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50089edf349ad41016816bf861fb35237c2e4e4ba588846607ecdb100fa624dd -size 41703 +oid sha256:652613c8509079b395d964c0290272a09b6a9bf4ca02c18d6edcae0b43e06d16 +size 61053 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png index 8df559390e..2ce79cc887 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5758b86295a5cd92cd103992ee601c762b876fec28a4380c793a6c054993513b -size 35016 +oid sha256:1ee8832fede93fcbfccc7e554a7528ae77a50a3a8f8d9ed87e4bff209eb62807 +size 35457 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png index 044901ea5b..d8f4384537 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce01cf6c3d326ad4208fa9832cab3e56164f791f59833b1f73d6f29e531dab8 -size 54709 +oid sha256:9b8b6dd8ce354e875e682b0f228bd9d4fc90b561202675aebd8f9bf83b228517 +size 55096 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png index a7ba678616..612c0d3171 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c52f77e77180d8e15b67abefac41b54107397fd489440d5aafdc4079d0100b8 -size 55075 +oid sha256:e9cf37d1b417d0626271087a6e1fa573ebd638192bc369dd376c67c67edcfdcf +size 55463 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png index 220ebf1ac9..90cddf85c4 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29ac70d223cc1f1c3a63c21431864f60bd6e199de574e1a2cded10bcaac11a8d -size 46842 +oid sha256:02f0679f2d5ffc1dc754f9bb317f765c3c49ee1e190d0aa234637c9a28ff8cb0 +size 47351 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png index 5150bb2206..c221f764c5 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f04c852c210a027d1792d7512aa9ecc3a6913d3ba9d18d8680df991f8050f57d -size 51086 +oid sha256:f753605522c268a0835a279f0680654bceb86f38ea4b76605fcb71917a77c7db +size 51594 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png index 3c5ff4c7c5..6a18dd3f5f 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0d6742849583dc21500d5dba09bf535f71a5bcd070cd129305c1dffd817d8b5 -size 49443 +oid sha256:820d30e4e8ff9329dee9d49fa6f0c28c5b1f5e4684db085c8e6ceb265fcbfb88 +size 49997 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png index ef8273806a..431e2101dd 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf746337b88e848b7e99e526efe2ba468d28fd1536970ddc4e02e0d946e026d -size 45151 +oid sha256:a403f77984cca9eb94755dd74f1bd6db854725347dc8f15a207b20cc2dd43ee6 +size 45253 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png index b75f0d76b6..36a305a42d 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63b5fdd8b50485c835ce04e61b3c21d2f6d306d73c98675d1368772f4ca74c1d -size 49708 +oid sha256:17c1da5810c4d8ae13670fac57d5cb57b204cfbed83e71b2bcfe5657e6bf354a +size 50247 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png index 0d842f530c..c40acb066f 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d7e27bf9c1e2fa0bd86fbf986ced9f21ceabddae14c00086b691f182aa7ec5b -size 45918 +oid sha256:0043a95fd7946c8247a963e38057ad8b4f62f4112ede1df7cf6e609167dd1e87 +size 45974 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png index b6587618ec..0cb58754ac 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f61af5729e24ee0b15af8df2bd641411ac8f17a5b7f9a76899c6d05f328dbd05 -size 48879 +oid sha256:cccbcf0324e642f49c739f079bdf7ac18876f4dbb99e23d504b1c22669a376d2 +size 49352 diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png index e41b40ead2..37b41517cb 100644 --- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png +++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f83a10070d130542d0783c8a39f588b869ec265b9c0026afaeccc0b7b50602e4 -size 38159 +oid sha256:a82d583db67f7507f0ca837b9b75897047808a1a7f3d828c7213a9a6f4578429 +size 38613 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_de.png new file mode 100644 index 0000000000..210020e06e --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99d5cf65c968619f2a99f382e2de07d08022f0325349f86571c5766d17fff9e0 +size 327894 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_de.png new file mode 100644 index 0000000000..3ee21f4b12 --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82b816075c7ca6898da2c295740f6828a15a16062e944f01a4401ac0b84f6008 +size 282325 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_de.png new file mode 100644 index 0000000000..7aeafb4481 --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11222c39d8ef830649fdd0b35c95b31b4d934c0c2667674ef7d659d9a3320b52 +size 255483 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_de.png new file mode 100644 index 0000000000..2c62689a45 --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc3d7320da4451c862f1083981d628a4df4115e95f9e88db7e0d1910f9890eba +size 276670 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_de.png new file mode 100644 index 0000000000..1ba1a3158a --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b23a3fd19bb6057dc13f61525a4589a3d1dd66a97818c4229d4db35cb6f56faa +size 250779 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_de.png new file mode 100644 index 0000000000..3f3fa22933 --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:028b3249d8d63cdf799ac1e0ab530e4a46140c53adf8b83a05153d03d030d4e6 +size 310168 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_de.png new file mode 100644 index 0000000000..aaedf662af --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24b6a36a3efa0127a099081ef711fa804879ddca347849580e80c2a147c7f4c9 +size 282515 diff --git a/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_de.png b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_de.png new file mode 100644 index 0000000000..5b2ff9626b --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7138b55da8a0a0a3ed2c049ec84d01fa298223a3b6ffe391fad7a7e3a3a3beb2 +size 282778 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_de.png index 3d408f0ee2..486830b06a 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c110f007c954fd01fad8d4e9810abae47bb89c330141c7c1d6412d3995943f4b -size 400985 +oid sha256:ec642dfd92ffcd3ba4a9ed06518481935c4e38342622c4c5c74efdd68a4e7190 +size 403979 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_de.png index 4ce254bb81..77e3ac9d5b 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af9841032d9faebbee6f759259ef14b0d4cca3424ac110e1dfd1d3357768e436 -size 399354 +oid sha256:45e5a33dc04d65050fd9098727f262dca526d5f55f2c64ecd235e9ae97aa8767 +size 402313 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_de.png index d466ee2bc4..7700d70b06 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fae17aad6ac7dfb2fd2fac83c2c1b6d9e0743fa9d3d1a874fe269284149ce29 -size 62760 +oid sha256:608236a6a1864f0036fb0d2bcd85d7269557dc3453876495585573471d08d2de +size 65085 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_de.png index 3d408f0ee2..486830b06a 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c110f007c954fd01fad8d4e9810abae47bb89c330141c7c1d6412d3995943f4b -size 400985 +oid sha256:ec642dfd92ffcd3ba4a9ed06518481935c4e38342622c4c5c74efdd68a4e7190 +size 403979 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_de.png index c45996cc7b..c9b90482e3 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbcda50596ea08167fda422fae89758bc46c719cd320103def3c90cea0a152ff -size 62616 +oid sha256:116502f69435e6d7a5fe9fe9a2a604fcca36d1c6e1e09836595769cf8e8022d9 +size 64937 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_de.png index a1ae4a1c9b..a1609b8144 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b1172f37d89f97ec0360402c013328ad65bfb62422d5b81a8d06e9d8dc3cbba -size 67739 +oid sha256:4d3ecea8217faca0d45acfa0a04023efff0124ad0563857ec4ce81e826033f74 +size 70311 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_de.png index 3baea3a223..a10d3f58f7 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29d6a3980b5928907e5d5203ba4a5010feed5ca67107ef464933717664d72fb1 -size 71937 +oid sha256:f350e78ea3bba4a80d22af4df58b8a6e415a68fe56770027c9da95da0cab0973 +size 327790 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_de.png index be72870cdf..f609ffd014 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df744636336e3b7dcf4b8c20a3a9dcf70247ea0a5602c11665bb020925c94dd1 -size 407934 +oid sha256:fd092ba5b859abc14b18702a613aa4b7bbfad43c2e91e294ad876edbcc5f9960 +size 74489 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_de.png index 48b54318d5..ed5e8cf442 100644 --- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_de.png +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea59b97a710eab6b10192373c7058b301e0d2b4bfe07d16f952d69593b8b78d7 -size 88581 +oid sha256:5ae9aeabad16587e233b815e60c76856f30f7296562a2b4abb1149a8490435ee +size 410888 diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_de.png new file mode 100644 index 0000000000..50674e05be --- /dev/null +++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a927ba77b12465d718d8b09ed12c4c04b18c2d9474d242a2c1a8bbea5cf3b226 +size 90934 diff --git a/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png b/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png index 14abdc0caf..198bd4553b 100644 --- a/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png +++ b/screenshots/de/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4f823bf8a422775693ba64ca10e087286967d11c8ed94a3198f108abd3a9367 -size 27026 +oid sha256:1ff21ba8a011e0aedbbcad919f46db1a26895ae6e286335055532f43dbf505de +size 27661 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 807e16c40a..6d9b506ac7 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:bbdd0d31786b29709c02e9f35191af1334b6f6a32cc0614a833f79d47551fc70 -size 42687 +oid sha256:cc44a5bef33ae51161dee412b729baf328b188976156d7eeb7677039ad60b7cd +size 42147 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_de.png new file mode 100644 index 0000000000..08169845d5 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86eac241e9084cc830ff13d231908da2eb0dff246aa5cac64dddaac68a5d5f13 +size 295298 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png index af84cc0319..1c168e3cfe 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:579709deae308329674cf4d47805485699f42c67e6b6779b78e95f45437aab4d -size 26045 +oid sha256:e78774044b40b63565ef540b6c1fae9fd97c105ffcc049e524e8a80d88435b50 +size 27225 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_de.png index 642cb48d0f..2a6974b789 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebee62aecbbc7e65168799cdeb940d112095d7e38059bf91e4192dea242eb884 -size 113883 +oid sha256:42743cf803a9acb5dac130af3de4fa08f78847567ce6aee1ec5596a96ed09e6c +size 197869 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_de.png index 0b33acb4dc..a8d7c0385c 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7bc6e29e43c98d47257304e38b5d6f3c0aeb55fc671adc02ea0234ba6ff31be -size 120123 +oid sha256:5dd0fa202f0d1ec868537a2726df10013072fe9cec7bce4c947ab13f39e683dc +size 122366 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_de.png index 400c371f56..abdfda2abe 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aec9126323f766fa0caf0bf1aba5ebd2110bfe6076c91197f4613088eb3b8a62 -size 120233 +oid sha256:1be36834af38c01a6c6153d1f6a65bb89713edb4c2641e2f2944d33b119e4bbc +size 197468 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png index 236409c967..04dde37923 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d83ed47c66eb0437193de92c356668ad479dd7cd3f97e864f121fdd01b3abac -size 48319 +oid sha256:b2fa0d69002c82ea536b1d7c6d46a93bf53bb484b1f995929661af4b1b5d77d8 +size 48271 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png index 3c80b38276..afe7a74f31 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a7d35ee4b53fa88a65380c38a39b8b512fa155d380458dda00fe48ead065fe1 -size 50606 +oid sha256:baf37ce8445ef13163eea35baad0c0e08a70d0cc03e9b8021407ae1b376ca95b +size 50574 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png index 25a5c7d18e..e38faafccd 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31bfafa9a3c71609932ae9de739529b6dfeb1038392cee97638311fc490c0015 -size 52029 +oid sha256:29af094f63268254fd2c67417ab4e4c129961edf2ba7076f4a8ab9a28bf19ad9 +size 51952 diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png index 34073d7f96..2c874ba817 100644 --- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1dce19e15e65b860ae2b8a1799ddfe49d4643582659530f7e6e900eb23a80ed -size 52380 +oid sha256:1241bde9ac9922970e2b8d1ff473e2bcc7a676057e5e62d26cdb1994b0100322 +size 52304 diff --git a/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_de.png index 67fec85a88..b3f6706cda 100644 --- a/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68d468cf24f004f75b5f0d0ab21e4576941749b32a6780b074fce5ba6e7e06c0 -size 56172 +oid sha256:29fb6c5ccbc3fc1196fbccc4ce18d10668ae47733ecfcc454354ff231cac0f10 +size 56030 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_de.png index 72c99d1da9..3ece744fcb 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b10f43a24d80689ee7c929eb5c530b544891cb0dce63ab9bbb4618f999fbd3cd -size 6226 +oid sha256:cbc7e8874d0a3c25aa0e6036c1ffc365c5407d79b011e5321821691315e98d3a +size 6223 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_de.png index 610bc5d0a0..b6f95124eb 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2b86e1326023be378c0c03b12305810f36e4fed11886ffd3bfa46ef9e5802ab -size 18767 +oid sha256:a2d68bc8db6190fbd5205fdaa386c250ae3a2014804a5372052d851a313a6ef1 +size 47808 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_de.png new file mode 100644 index 0000000000..94428abb56 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 +size 347897 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_de.png new file mode 100644 index 0000000000..67490ba700 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 +size 369730 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_de.png new file mode 100644 index 0000000000..f0dfbc3321 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7a6302320bd1e4258e03487539bf46b0b12781dfc8a22b3f9f267017a6146fa +size 354751 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_de.png new file mode 100644 index 0000000000..c4943b892c --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c410e853089cc2c4a2c48a00dd9a8babc8095aed72bb4625057444710559c6c +size 368129 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_de.png new file mode 100644 index 0000000000..8aa2cca147 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02ecb903124273e24151ea2088f6b99144f7dcadba00a582b6210dd23835b5c9 +size 357743 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_de.png new file mode 100644 index 0000000000..cececc229a --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0c6dafaa41849af00d5097a85bd88e3a054a0a9e41c0466a08e6ff8dd6102b2 +size 356857 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_de.png new file mode 100644 index 0000000000..0041a870b9 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2bd77c08be705c66225f16c29fd6826decd92b41922d0f8ffc0d1a63d8c59d2 +size 356193 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_de.png new file mode 100644 index 0000000000..9fcf389708 --- /dev/null +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9daeee1aaa04247da3c8bb5aaae9947aab1ed2d2057995e53d6ebdb63e5cd356 +size 355848 diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png index c4357e38dc..cefd1decbc 100644 --- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png +++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d9c7c8dcd575bf6376edd385e32b489ff322944a323df989a30e8779fc59f25 -size 56604 +oid sha256:aa9709734259b3aeae5cda5fd0cc9553ef741fa3712b5ff39fc27d1690219d3d +size 56460 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 index ff53dce724..7ba4cfc7cb 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ebbbddf29df78c6a6b37f931052cac67ea7ae1c875e2d75fa6ae8aa1683971b -size 87348 +oid sha256:20d88acd1cec244527f3f1ae3ac49cdea7ba2e0f4710ef30eec90f8eb5d6093e +size 87253 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 index 75eb3ccc78..abb112a538 100644 --- a/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png +++ b/screenshots/de/features.messages.impl.timeline_TimelineView_Day_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68cef5541309a99a95f5004cbdcb4d091a70f68c142de2ff78deb710c521ea30 -size 69820 +oid sha256:4c119e44d9f51d0b4786f06092d3d0138ef69b363c43303164b0aac0629b324e +size 69840 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 ba7d60941b..2cc385f926 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:e916a49d3cd4d81d8ec5f3d66f0b930bfba6e9c20f1b9c5f7d4c0bfe51895036 -size 67398 +oid sha256:a6325d21da32e6def01f35803b4823d037076f0c96c24d864fad33b0a7d9a7b7 +size 65731 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 0d70a656c2..1d36e3c163 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:86fa2e5742ba38c173add0921615a18c1271caa783fd1a505c80a06096a3c248 -size 71646 +oid sha256:853058d4d90fc9d39115263ab74e7813ab6a52844cc56cc4a30c3dcb42e291b9 +size 70016 diff --git a/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png b/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png index 99f0c161e8..2190897314 100644 --- a/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png +++ b/screenshots/de/features.messages.impl.topbars_MessagesViewTopBar_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d23534a940951085437a3c739b58d515a968869227db794a149b5066abe21800 -size 58473 +oid sha256:f952ad332aea788e87445b304894f0acf77a6ca5498d67584ab4e727cbb16eb4 +size 58055 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 98dd645229..63497b4081 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:9b13eb680dca1d551d843b92dff0062a58781c6c9c1fe713e35c26412b6addf6 -size 68596 +oid sha256:a7cf06d5921dae7bbdd3b774e38ac6fcbad724261c1adef9ad93ff992fcc7669 +size 51515 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png new file mode 100644 index 0000000000..98dd645229 --- /dev/null +++ b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b13eb680dca1d551d843b92dff0062a58781c6c9c1fe713e35c26412b6addf6 +size 68596 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png index 8e73293bb2..5f24ba6d45 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1afe6a244e864befb72decf8d2518b9588b3f59b5382c8e5c6130a2ea33e23b -size 41952 +oid sha256:c100900785c0c11cd69b60da9ed3909f135b876d0546722f707febd24e796256 +size 42510 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png index b713fac540..194a69f62b 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e08f56a74a85de1d5e792dbc17a409f95c0a72df49b3c2538814442fdd63d4a9 -size 65018 +oid sha256:cd28a4f1e30c5ef7db0cd167a3d0328af7b969ccd7c35f616472dc2ab02b5c4a +size 57867 diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png index 63497b4081..b713fac540 100644 --- a/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png +++ b/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7cf06d5921dae7bbdd3b774e38ac6fcbad724261c1adef9ad93ff992fcc7669 -size 51515 +oid sha256:e08f56a74a85de1d5e792dbc17a409f95c0a72df49b3c2538814442fdd63d4a9 +size 65018 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png index e0b6b5510c..8c4b6af1e0 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acffa3d209c390caa581e5afdf4d31ef5556e96eeac1d136cb7f24a9bc9996d3 -size 22234 +oid sha256:bb05198bc007e836b105f9139f7deeba673cd2a7664c53a85e25e5fbec2d2052 +size 22188 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png index 4780df6550..11b6cab54f 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8387c3a65fcf21312934adcb97c85f18e2764deb774c38821d360eed2f97101e -size 21960 +oid sha256:b73aa1594f1e0f6ef0903ba7bf69e48f50b6904511828213fd2462a75b670894 +size 21912 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png index dd7fd4540b..132504aee7 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbc74888e9ddb1123ebb9e6d1f0851b55cd68b69b3bf284ff41d5e8f394e6999 -size 22042 +oid sha256:fa73db75c4d6bbc88359aeb8348eceb2a7775290218a1a761dd0e993427b6435 +size 21992 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png index 702cedc003..46f744276e 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed21a6bc055bfc7964bd0268cf4597350786e5c58c869d0fe47d3938f41e5af4 -size 21995 +oid sha256:7ef4b1d40c17d7e949d051c275cac81db5f231214499ca6f8f40d36b7cbc3e32 +size 21952 diff --git a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png index 2ae2780a01..9d8e17945f 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebef4dd474d40bd93a5448a851ea8d1400bd14e9bc625d091ef2265764a6f01f -size 21880 +oid sha256:ce03160f5c2c13b3516f75d64ec311331d1a99d7c72c2112843b25332d2de042 +size 21841 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png index 9b8bd6aba7..b4f5edc8ab 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80411a8f91e1cbfce66887dce4cca84155199e2f2aba492526665952a09ce9a7 -size 52353 +oid sha256:525984fd154320f88445fde7298b1509a86eb8691e5a9a8cf4c58d8fda9b01ec +size 52272 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png index 7e89601827..014cb01e47 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf3fcef9ae6c81f5f063c230a57f3b4dcbb5c6f1e4d08df176df9712dbe209e8 -size 48711 +oid sha256:b00f990de2e706b44b81ce25cbe63eca682a8336931079f5b3d06c8fee57a14d +size 48644 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png index 25a5c7d18e..e38faafccd 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewCreator_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31bfafa9a3c71609932ae9de739529b6dfeb1038392cee97638311fc490c0015 -size 52029 +oid sha256:29af094f63268254fd2c67417ab4e4c129961edf2ba7076f4a8ab9a28bf19ad9 +size 51952 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png index 236409c967..04dde37923 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d83ed47c66eb0437193de92c356668ad479dd7cd3f97e864f121fdd01b3abac -size 48319 +oid sha256:b2fa0d69002c82ea536b1d7c6d46a93bf53bb484b1f995929661af4b1b5d77d8 +size 48271 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png index 7e89601827..014cb01e47 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewEnded_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf3fcef9ae6c81f5f063c230a57f3b4dcbb5c6f1e4d08df176df9712dbe209e8 -size 48711 +oid sha256:b00f990de2e706b44b81ce25cbe63eca682a8336931079f5b3d06c8fee57a14d +size 48644 diff --git a/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png b/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png index 9dc06851c0..69fd570f47 100644 --- a/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png +++ b/screenshots/de/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:543af353f726b5e68f22303bd580ad2a827affad3618140dc7ee87c12ee0d9a3 -size 46153 +oid sha256:fa2bd8a0567d05208a570830dfeea3a3f123bfe7d310c777c9f4a36d942d4583 +size 46081 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png index 31603b0b30..e1fb023c6f 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b443e0ceed54e76337bc86cd278efe22e46516659f611abb5ba166a83eccc1a -size 41925 +oid sha256:b35f616567e549a35896b1136b16079d2e0582b055cf263aa87e18c004b454ad +size 41837 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png index bded534a61..d9881bea6c 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78b18d7bf93b1efa40e0746b32e3a8228e49a063d49628c7e18f1ae7288e2dbc -size 42104 +oid sha256:cbfe9f0e3841f6b2936fefb3a10df53339d8b1d254f038872bed02d821646c85 +size 42090 diff --git a/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png b/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png index 2e504e332f..d1275f23c0 100644 --- a/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png +++ b/screenshots/de/features.poll.impl.create_CreatePollView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cdb9a9e2a5af6b2cd8a43e07b79d0925b317210f85c714a1b26b529870752b5 -size 49372 +oid sha256:5c65f083f190cc77b33fc17f0b75cea11571d78aec0d68ca0bc965691f5a4de9 +size 49272 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png index 7731d6380c..a51cb6cf8c 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d3cc301b373d6261bc57f91ff1f83485268f23add4864b75471b8879fd2b7cf -size 59648 +oid sha256:d33aaff7f6b61e202d114f91b3c4bbdc9e7554910b1d7bfa8aa463f508927b65 +size 59530 diff --git a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png index eea3b79e31..9cdf321360 100644 --- a/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png +++ b/screenshots/de/features.poll.impl.history_PollHistoryView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26284504e5cafc2c5bcdbc804a0712d4644fa2184751b76b6762c75c8b6d1cc9 -size 63811 +oid sha256:c2f1237015fe09c45636a00b3bb5991e6b1c544ab129cdf0f720db881e0f2f50 +size 63697 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png index 20df66736f..0793b01c92 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2c1dc2c23f6b8dafab3eec3bd703c9ffeacf8e3de6f0da8b182ded7dea27244 -size 53656 +oid sha256:cea139c9bc1d71d342a99ce43b6bff414e1f9cb53ac69b8994b1b7ea2c750f41 +size 60894 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_de.png index e9d7ece4cc..c50a3e35cf 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2215fecf0a0cd2e708d7b2aba1879528d3a1ea07cc2dfa255d74986fec933ca -size 53531 +oid sha256:e19998bcd7bb878ee3fa1429ce7edd03293e8f9dfc2dc9cd439ad3a71ccd6276 +size 60764 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_de.png index 5ed68bb027..eb4c703d89 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca1bff9d9c6f5b68823a43eebc8196ae9705f743e7b59897a69cc4016ee155ea -size 53523 +oid sha256:139462dbd69c23df0aff5b8a2b310c2a9a40a9f691ebff57a03d0bf7cf638269 +size 60757 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_de.png index 7e71476836..4e5adc17e1 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c208bdd8a985ab7108020659f0c6dec3413fdd54a529f86d7f2bcb44448fe0d9 -size 53520 +oid sha256:3013b0a0e261f87364a5e72cce9d25cf59c2d1360dc778fec533268e1340a0ca +size 60755 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_de.png index 46d1ca20e2..33a8e007e4 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abe747567a372eb4b674a1e7976b24d498ad2a4d7e20d425c9291ddb05922a14 -size 53339 +oid sha256:a8f056723e3999c53a1e582237290b74e178e652fd49d5b636d1256bcb846de3 +size 60597 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_de.png index 567949e6e5..61e1649e93 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90068ee2e83b713cd11d9879bbcc3c389dd0f79778d8999df6bfc9f2b1b1bb55 -size 53662 +oid sha256:db5aa655ec48b8b8c3019e886a742a2e7922f329c7d7e7c88ca5d70cbf2f6f3e +size 60901 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_de.png index 04837d0ca6..f48a864d6d 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3387219f505ee6168eb44879b4f943b75459f4c5b6c311cce874eba5e49ba628 -size 53108 +oid sha256:0fdcef2b4374ba9a1dded6c7fe3315ccc3c585e489d59572879a1e5efe2dff1c +size 60422 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_de.png index 304befe3c3..3c66dd1128 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7333bd76d2de37bd13fe507d89c000984d73141a3dec81f71eaa13d6658871e -size 52600 +oid sha256:ee1b3b15e7f08f2f63c556a584db2fa3ebcf004b980f7fe837c32ea2d166159c +size 60000 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_de.png index 025b3d596f..1eca9c9326 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97f271f0ba965c1b40edaad388e111bf624c01e5caceb73dfae1e09c28b3856a -size 61504 +oid sha256:1dca13f4ac7e2d0f80d302d2d819a32f536d5765b2d7533be4f019b6bc28dd92 +size 63485 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png index 0bf95ba4b1..246e46b33d 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:030671a61e231a9c87a221e20e63bac8add5f0a3fe4aa967313af45196151c64 -size 51196 +oid sha256:a060a4ef3ae0e7117863606fac9196536fce38d5483e3d882e86931d5f6098e1 +size 57951 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png index 8ece7cdeaa..248175a402 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c813fcb2b9cf76749d9233d5cb66e643e185ac3248eab4c4fdd71f4c7fab7ba4 -size 51057 +oid sha256:9e4faac64c7b38a8b9613eddc62805a5816ed340984e62dc5d6625a79782d3e6 +size 57811 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png index 89c8b5a536..c8bc853a8e 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66feb2ac420379ece0b85ffe53d5a25a8d11cbe27133313dd9e679d582c384fc -size 51052 +oid sha256:7e4713a87444130061d605edcf65d67d5aa1d6730e8afee1ba41c751c2dac77f +size 57804 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png index cff6d2b2f4..19d124ead1 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fd601997ff6e5af28e70c25c7c8c2a61c37e47e736b955b1510ac67e3a05034 -size 51064 +oid sha256:d8907dbe4553c7aac67cd5007dffc12a06e3c3f218fa95a75c390ee8982cf117 +size 57815 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png index fba8ff45f9..26bdb4b258 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b089d766841cc1ce24ea03d3d91b59d0f2ea1789aee3a9d06ece551c862d1ce -size 50901 +oid sha256:961a4dd2ef4c74770f9c140f4b337146225d7a626ca2082f35cc107c91263e12 +size 57670 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png index fb27f001cd..dca7c97656 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b38f2502dd31806ffab41547b66b9987de66c4e899b0df59c068d08ed4350b6 -size 51198 +oid sha256:0ea1eb184dcfcf13c8673f3df717a540c74eb500b759cd752e8fd270da9b8f4e +size 57951 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png index 0429803048..53eb657d5f 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ce5af26d03b80ad7bb0190b6b1dcdbdba4f7a666c442109da58f05c07505aad -size 50867 +oid sha256:ad17f959304774a8956cc38cb008024e79ae70dd1d2f147b2de20a95ca29ca96 +size 57638 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png index 27f6862b8c..1d51b69a1a 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:536cbfdce1cfdecdbf076f0747c50f2db7e3a88dda54c0708d4b7388c9efbf30 -size 50402 +oid sha256:85693d212e3726d4e7bc5cd3c4c2e9d73276cac7a6cba7b9549d5b45bf55b686 +size 57160 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png index 0d41a18697..5736128377 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20285a2d5b10f74f927f8459a2d92180b7f86bc266c95889b5d017679b622462 -size 58368 +oid sha256:aca6e0c7f9d7c48ca80c5a8e56a86e8d0a1cced6020d5b4ef0aae443575071ae +size 60239 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png index ab828c858a..36df3fb547 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ff37f653ce34795f7d054e1d552c161c83fe85b6c80c6925953440a75fd4ea4 -size 53181 +oid sha256:7713d5bb6252597bfc465fc85b3283e08d95684f9d0a17fc89e08f5cf8e870e4 +size 60327 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png index 6583fc352d..833794c51e 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d69ff7dc1c3efb334b8a03dbd796cde640aecac07a10adb910519810bddefed -size 53065 +oid sha256:59b93100a819cc8a0b8b8156da39f47034745bfc6d06957907009ff3eef3c2b0 +size 60210 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png index 1080a3e297..25914b7cbe 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:827f3291c252fa8a5b938ca7a38a76d0d58e1969607f2c5514d06264e10a1ddd -size 53078 +oid sha256:eaaf5ae88296f64e80ebbe60a126a1fd40944e31311329cade209631f36424cd +size 60223 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png index 1b77b7d963..2af3d0fc1b 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb8636f0a7dbad6b32346cad952b5447a79b96d3db0e5da876fca7a78ced91c2 -size 53072 +oid sha256:77c71a0c84d3fa9e39ba3c9b9b81e8a3b6e406af87e4158bfb8bb4736a8b27c7 +size 60217 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png index 3048b141f6..9f0b5c037c 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24dfd7e781f8b035045ca075b607676a2a4cbc0ef882c1f527252d85339a738e -size 52922 +oid sha256:c9228e4ace7d30444a45deb04f211782ca53c5a366e0e812145b5b772fa51d3b +size 60085 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png index 8ebc7b90cd..a880ff90f4 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:640ecec1abd4992b962a9ab8ce68d4c5a3accfde9116b93af4d55d8365c35307 -size 53187 +oid sha256:c00205667f22842c1c5d63c20c0990b6e0fb7f7cd134f625957df733627378df +size 60330 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png index c1dda2d9d5..4265684997 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc3da8b4380809f5f9724d98ba4eceb86ea9d6a7df54c4a2117d187550b46597 -size 52800 +oid sha256:cbed351c3ce6dfcb334d28316501baa5f97ade4614bd9b7dbbd91b0ce7689994 +size 59987 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png index 419eec6b72..dd801a4745 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c03b9b002d3d5a42c6a5662301e8feda20f734a299dbd753b4fa797023ad697f -size 52372 +oid sha256:09bcae54d99e6b9279794ebebf32a67f67a8347c94f3286243fe8d2c0a92f352 +size 59623 diff --git a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png index 75634a5633..2e74f12057 100644 --- a/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png +++ b/screenshots/de/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7abc46138c98086dab38fc368d56e06ea6a434bfe61b0023a665b40e66f0aa53 -size 60801 +oid sha256:5d0ccc252fedce22f568f8d26f25a8600c21236e1d489a4dd59ccd63c1085227 +size 62771 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_0_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_0_de.png index 446b8185f5..3506d44218 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_0_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26f2620aa348febb2db9ebe379a964b76220819f7165fa7a5be5e2a38c2534a2 -size 67183 +oid sha256:cdde4c4250683a2cf63988a92dc03cbc64581794c9f25567ecca61675edea9dc +size 64428 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_14_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_14_de.png new file mode 100644 index 0000000000..4b59292eaa --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_14_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:168c2399ac6262c65e1026e3c79fe2c93dd6805ac73b9bc26adfaad8afeab724 +size 53258 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_15_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_15_de.png new file mode 100644 index 0000000000..de235a8eb6 --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_15_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:950b6b93b0b31d0bef28e6b7ff0759e1e6e3ee48fac8278035665247f1176e8b +size 51887 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_16_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_16_de.png new file mode 100644 index 0000000000..d46754d4f0 --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_16_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d53770762a7b2477787fb21acb17303b671d549b4eab5f54ccbf6d1c566aa50 +size 53023 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_17_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_17_de.png new file mode 100644 index 0000000000..06c3f12377 --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_17_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32abcd8e957508558dde739c881f2ce9efac648404c420aac44127d0b449d5c4 +size 53000 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_18_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_18_de.png new file mode 100644 index 0000000000..6e9281a6b2 --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_18_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6bf8bb2491b685153fb11542be0de2a95cdd00ab294cf0bf882e890f8f5f697 +size 48682 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_19_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_19_de.png new file mode 100644 index 0000000000..7439ba1f73 --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_19_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4f67eb6df70b214682b9b337886cee1653c3804be2b3a6ed581247c22fc0d5f +size 44778 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_1_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_1_de.png index e0fac995a2..06c3f12377 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_1_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ebab37f3d289cd684d616961b2f4b993a851a34bb8d41ce889443ce51c4c5f -size 53981 +oid sha256:32abcd8e957508558dde739c881f2ce9efac648404c420aac44127d0b449d5c4 +size 53000 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_20_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_20_de.png new file mode 100644 index 0000000000..10bda48dc6 --- /dev/null +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_20_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:058a2ef322426a6c996698c5c0df64f8d4cec5107b918101478a6f478897c3c1 +size 46365 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_2_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_2_de.png index 286ee2d3e3..5cd18f7bfa 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_2_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d578725c9ef0c37be7115017fe1b7f7da41c02a54f932ee389d4683f2ec3f524 -size 48465 +oid sha256:78109755f9bb674e0b026f8696dcc5024e1aa07838d404954e17edcf8d752e80 +size 47441 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png index 8428aafe61..252f3c7397 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e949b9fc728afe5da33073a5273aebe797a7c45a68fc6049e9292d0a256adad7 -size 49632 +oid sha256:288f3c3887439cfe8b4fea6ba7b5091c2342cf9e962ad2ddbbeea99243feddaf +size 48577 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png index 8428aafe61..252f3c7397 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e949b9fc728afe5da33073a5273aebe797a7c45a68fc6049e9292d0a256adad7 -size 49632 +oid sha256:288f3c3887439cfe8b4fea6ba7b5091c2342cf9e962ad2ddbbeea99243feddaf +size 48577 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_5_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_5_de.png index e0fac995a2..06c3f12377 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_5_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ebab37f3d289cd684d616961b2f4b993a851a34bb8d41ce889443ce51c4c5f -size 53981 +oid sha256:32abcd8e957508558dde739c881f2ce9efac648404c420aac44127d0b449d5c4 +size 53000 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png index b107216acc..dbc959895b 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23548dc4c9b3ede93580ce56989ff05d4317605aaa584744f73a5897d0cd6c51 -size 46743 +oid sha256:56fbcdd514a6fc964e0713ee18490f801bee4b5b91f1ebbf9471262af37830e1 +size 45656 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png index 058db215b6..9d81cecd3f 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a284cc8503c3cd6227afbd593d36e6f9a9eeadbd323b5cf66d77e3b5eeb6681 -size 51713 +oid sha256:a2bc82e2fa80c56ef65b462421c4b0204e6077547226b799c5a9a47367a1dfb2 +size 50724 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_8_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_8_de.png index e0fac995a2..06c3f12377 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_8_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ebab37f3d289cd684d616961b2f4b993a851a34bb8d41ce889443ce51c4c5f -size 53981 +oid sha256:32abcd8e957508558dde739c881f2ce9efac648404c420aac44127d0b449d5c4 +size 53000 diff --git a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png index e0fac995a2..06c3f12377 100644 --- a/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png +++ b/screenshots/de/features.preferences.impl.notifications_NotificationSettingsView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9ebab37f3d289cd684d616961b2f4b993a851a34bb8d41ce889443ce51c4c5f -size 53981 +oid sha256:32abcd8e957508558dde739c881f2ce9efac648404c420aac44127d0b449d5c4 +size 53000 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png index 99f738df12..9827329bbd 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe130f936313191f602331d5b1a08dd025da47a66ce23dd71cee0665e951a3ac -size 44506 +oid sha256:ece0f3e49ca489a67aa7a489bc2d7e4f35a99cff4e4f2df3f7ab480965b3f972 +size 44150 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png index 47113e66d0..28e4e403fe 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1228309dd9c2dbaa340f1b941ca9da17067c619a8645e870f20bf39db7c9ba25 -size 27560 +oid sha256:2b8741879622a799c9cae5ba7ecb0f83700cdfca52dd9e6b281bfa0cc31267b3 +size 26817 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png index 12791633f9..2f7d54a7ac 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b095c101503364437c57050c56fbcc56e8187a697921364eee4ae0807ad684e -size 38449 +oid sha256:61958e2bf98bcc380bca8622d53ea8d46ec466de6e8da134a9b01ea1605eca6c +size 38040 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png index edfa967da9..61aba383d1 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:128e6931b4ac3fbc02138fba8a67e22f60e722acddd8db26b1b031102bcbb198 -size 28060 +oid sha256:3e8275f65773690627383ef7f030f270c7f93639af5dff40ca46ee0c2d5e0316 +size 27628 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png index efb26f07c5..672f329d0d 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc9e3439b97efbd490030a7fdae43bccd60193b18c658f4ced99959550fc77bd -size 29481 +oid sha256:3efe6d5c4b5f02679eddf9be90af0ee0648a576a4488c021d9d7601aac3c71ab +size 29060 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png index 01f0712a7a..5d238d5c65 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewDark_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca99f565a56588d126e357df4417469e39ff196d21d41cdbff05166b8356da1 -size 21419 +oid sha256:17f591ce8aa0dc3c79dd450dc58b84194625f1eee1b8fb3d5dbd67b3b1e07966 +size 21035 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png index 2b260ff9f6..ba560e522f 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3b21d14f077ebcaa11ac2dca31fb08f9dc21f26a4847c6d1c623f0ec953cdc2 -size 45806 +oid sha256:7fff1e6cc2fe7ae2fe3dc81ee1f13f096245c238f3ca946ba4a353e693d0ecb8 +size 45468 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png index a584834ca9..893d7a086f 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bff3f28233c7b1be7c3524f06258dfcf27dffa4251279beb2bacb883c3c86c41 -size 28576 +oid sha256:e335a7132330e2c5dfb3f4cc015e25fdf67af2030474b347df2e0cca343b1c74 +size 27763 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png index bd510c21a9..7a2abd7f42 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f3ff8d40a465f3d73143de0d66566c0129a794cdba9556bb54423b2110375c8 -size 39033 +oid sha256:645d02f8d4503edfc2e5fb756050ed1ddb8a692d715d06b84b232e744379d613 +size 38820 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png index bc7f862d77..3b526c2fc1 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a7aab1f7cc5a26672666fab13a36fd7bb47d524b9ffa172bac322c1ad1c3b40 -size 28583 +oid sha256:4df58e4d7f0baeb0bfd0225302ed8e69eba1edc0ef37ae6cb8b86ffdd057b924 +size 28414 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png index 7cd952fef4..57bed01c00 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4743cbd8f565d114a74024455bddd999df95fba4c0ca346b32203cf6fb6f7550 -size 30203 +oid sha256:1a24be625e8a7d6eeb46f0066247bc22bd367b0e035802b54a663478392f4289 +size 29980 diff --git a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png index 9cbde40654..4537f0ff7f 100644 --- a/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png +++ b/screenshots/de/features.preferences.impl.root_PreferencesRootViewLight_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a7e61a79e28968dfe2f57b75b88125471fecccdf4af670c7f2ca238019fc31c -size 21527 +oid sha256:f7a69722f7e5f6373a266116e7d97bef5e620b136deb2cea30409c41ec8d4180 +size 21310 diff --git a/screenshots/de/features.rageshake.api.crash_CrashDetectionView_Day_0_de.png b/screenshots/de/features.rageshake.api.crash_CrashDetectionView_Day_0_de.png index 29240c9037..6fe8d49af7 100644 --- a/screenshots/de/features.rageshake.api.crash_CrashDetectionView_Day_0_de.png +++ b/screenshots/de/features.rageshake.api.crash_CrashDetectionView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d34090b6d2a05946f81c205b40dba420f49c19063b8f4bf2d0f55afe31886e7 -size 24441 +oid sha256:225e3ea78d7f1bf3bb9c2c56c128bdb6994b22df3f7c010837ef37bbcdc34997 +size 25170 diff --git a/screenshots/de/features.rageshake.api.detection_RageshakeDialogContent_Day_0_de.png b/screenshots/de/features.rageshake.api.detection_RageshakeDialogContent_Day_0_de.png index 39aa8e9965..c78691eca9 100644 --- a/screenshots/de/features.rageshake.api.detection_RageshakeDialogContent_Day_0_de.png +++ b/screenshots/de/features.rageshake.api.detection_RageshakeDialogContent_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2e6807ba4b85c71cca8aebaf34a22c836c6d53ebf8caaa42790594d3066956c -size 27850 +oid sha256:7546c33148ccc5d46e112c2488c6aecce925a9a0d729cc9c029c9dac2578e2a7 +size 28753 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_0_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_0_de.png index 42885de40c..429f3b3bf0 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_0_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f935cbeb516a345549877793ed1e0e5d82b0b1b81b65d54f028d8ee30a3eeb7c -size 50075 +oid sha256:b3309c34f8127f41b85f915246d87b257befd909c6ea13151f018f2af838cee3 +size 55553 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_1_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_1_de.png index afb28a7513..3386f18dd6 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_1_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54c0d5db8026616d766d1ceb94fbac1c3588be1ac3baa38e05ef4f665aa9419f -size 115721 +oid sha256:4d11dbfc86f54a219d989672e5a5f2c729e1373706be22256aa913d3a2f67eb8 +size 116355 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_2_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_2_de.png index 4b03d29ff9..46a240da00 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_2_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8efca7b9be49bd84f809fac58de983e6126714914250ce4b0dcb96a56125bb7 -size 46915 +oid sha256:2a786030fface63e75009179d2473c5ec880cfb194ba43a85f2bbef3090a205b +size 52774 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_3_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_3_de.png index 42885de40c..429f3b3bf0 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_3_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f935cbeb516a345549877793ed1e0e5d82b0b1b81b65d54f028d8ee30a3eeb7c -size 50075 +oid sha256:b3309c34f8127f41b85f915246d87b257befd909c6ea13151f018f2af838cee3 +size 55553 diff --git a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_4_de.png b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_4_de.png index 473b887d73..6b799d3180 100644 --- a/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_4_de.png +++ b/screenshots/de/features.rageshake.impl.bugreport_BugReportViewDay_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3d7c97667774e00e618be65e63423d9d62c8b30dfe95cddad1d5dc62255e38c -size 39638 +oid sha256:90360b93852bbcbcf694f892075401d9019f0b28f34abbe24f3546a9db62fd81 +size 44307 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png index 759f942a33..e3f488bde5 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:088e737cec6fb5fbd624a16dbbcca45a8815441b93ef4d949acbaea9d9c52c3c -size 49472 +oid sha256:4d7c45ac3f5fd12f909706a1da3aae7620472958966eac0d49ba3020dc52b312 +size 46054 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png index 10ad61fad0..1022a785ff 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:f2f53b1fbc09307314ad92c688ef26490520de7380d27cab1ca67aca6e34a655 -size 47718 +oid sha256:41931967f6bc1b5a1244764bf1c9b8bf498f107edf8d85f6fd07ab3d6c5f2468 +size 44146 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png index e95dd4de1d..3a8ba95641 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:7bab8c4a37f2d9fbd4b29cdd729db9a84216fcfae3023facbfb979ecdf4645b6 -size 46428 +oid sha256:98e3c0f5bd719b4e5c47852e8e5620fa79724cd48d9360c4f2064f8a650555d7 +size 43499 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png index 32193ce5f1..00ce182ff6 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:b6a11d9f2402b756b61a5ab3bb766bf19404fb24b306f86634650b070554268b -size 48264 +oid sha256:e6da9e5dcfe52ebd26e4b590eb7cedb751a53729344556384d04d66da6f8b9e0 +size 44489 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png index a29bbd09cb..f09ef8bd09 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:b37442dd675bf8ce133b57484ec8dc11c695df69907ead762fa4b79c7c978663 -size 48172 +oid sha256:c7db4c8739f40a186a84c8420dbbf44c53aa1a079c74437348d651eb91482e8c +size 44445 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_14_de.png index 0fd4f66fcc..3156bdaf97 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:fa879e0d1483b07c3127ed07da87b8aabcc38c51db6e158ff29039e3b1886745 -size 49071 +oid sha256:3626d055f83fcfe23d0cbd79b2be99fb2238ce4e5da4c80b17bc20353849c32a +size 45859 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_15_de.png index 6100b21322..cd433d98e8 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:05db3b3e42f59515ffad1c522496ebff6092bd458297ca92eba25b321d8be0ca -size 49564 +oid sha256:8e1a5175e48e200145a132a9b87cdde64ff9c5bcd14e049c7ce81a27e11c9661 +size 46222 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_16_de.png index 63eb0980b8..368a0713fa 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:5662df69d4f1c66bb47019096df83031b190d7bc8e0b354c06f09d7c25d3773f -size 48454 +oid sha256:1d192d1d08b4bb235e894141d79d7fe0a19e5fc7d1dfffc25d9b7b797e9f5a1b +size 44681 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_17_de.png index 44c943a262..786c66da58 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:81ac09d41d5f08d8f4714f070c3661e0628a0d28f32d58eb0a1460eacb1be8d0 -size 47752 +oid sha256:e30a3b6f61cfa1933989eadac016b2dbb565634f95bfb118c3a0c248bbbda9c3 +size 44129 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_18_de.png index cf0d675e49..7d0b7034cd 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:2dadbbe0ad65da59d7599f782e3893375867821ba0b8309c61a94f530cd58965 -size 44846 +oid sha256:91351802609eb6468d1f544a403246d88c38a58e76437ddeda36d1530499475e +size 42646 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_19_de.png index 3b5b87ac98..3bf58a45d8 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:1d8648c9c6297f2e8ec87b334e1e885555e921faa7d98dda70fffb902ebe2688 -size 44828 +oid sha256:24238416160eb851769d2f6634e267dba824450e93fd7f193d5d6e3806741afc +size 42641 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png index c418b7d0b4..67d3aac6fe 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:bc469282e83bb5e88a173fc8a4500f8260f37ffc8afb20449e1126aa50631693 -size 43177 +oid sha256:6ccca13029b6f5fdbd79bd7cdacc90bfc4e4acea0ab59416785cd1ed84f0e110 +size 40637 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_20_de.png index 8360619979..57782baea0 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:3a0a8cafff67aa2fd2991ce4320ed412cfebed0208b2d1c4e8bc0f45e7977e6b -size 48534 +oid sha256:df391568836eb548b3226c88ef9bfc60cf62ef8bca9eb20dde68c18c06ff9e3c +size 44645 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_21_de.png index d0a134673b..93bb4e5916 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:3e1625ee06b9ae0025bf798705c8919b405f9cd917d85056623618fe0223e736 -size 48450 +oid sha256:615c16c1d98590e0d4cc81222a1f8fdc534681f5ad99ce3cda82bc4f024d555c +size 44587 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_22_de.png index 720c2eeae6..b7ec17f054 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:0aaf88dcf5e06d6c409318447e154b9d0b33775ad3e3aa8d8e3fe2b85aedd816 -size 48017 +oid sha256:abf088b6f30665a9e5d9d8a60ed170d0b8b7c219e8b71558ac9900986de1ef54 +size 44251 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png index e95f8da31b..4db528159c 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:ae9087dda9b2c6e160aa14dbb45e8ab614f2c315158c5bfc48d5c67058ebb63c -size 42639 +oid sha256:70bf1adc246f1d2b792048ea5739358c39935daab42b9a061241dffac4f8501e +size 38503 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png index ac8ea4aa9b..c645f66027 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:2d1c6f0c3898e000774de48cd7590c44bc11c297163735f16cc2dccaaf23e538 -size 45428 +oid sha256:42392b475d4ef7351a0f91d2803beaff53c99dd2e3c682e6619c8edac638899f +size 34399 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png index aeb6c18836..ad9a59b471 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:ff3ddf54027d1f77d8e1e1f5cc063f1c13fc2725447d7cb6475c2ebb6c84b529 -size 47002 +oid sha256:15f8f4091caeda71cc1a3cda9c100a0b5aab7bfa6feff4e70e6f2308258248b4 +size 42985 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png index a600833dc2..49d2def3f5 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:c99f882e777e43d1689ffa80a5396583426d987725424c779eff019f96d1c1bb -size 44444 +oid sha256:2f0c96227163246012a54fd985b282386231ffb9b03952ad4c2058500a081cb4 +size 42356 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png index a4d831a49c..b8f184af2c 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:35aab3535b161bb3b111a9a352f510b43a25f9c98a7e5648aa3a380cd8402063 -size 45969 +oid sha256:9406cb785c88cc1b370678140b57b5928e46616274529853e7700ad48768feed +size 34945 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png index 9d4d6b35bf..e9a027a9d6 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:6d637a9ab06a5eb79bbee7d141be92563d1c6d4acbb301b61a8c02db53e89132 -size 49078 +oid sha256:68ae4856b0ea5e73705d83b927577457695b8e5908c7d71e029953ee9cd8d8a7 +size 45124 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png index 0884641063..5911a811cd 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:5f45315776c2e841043aabdef8b72a8cd4e485270ada198b28f92de8f55bc7ca -size 48392 +oid sha256:4883dfc428a83e97ed406b172c91772ec7b51c9df074b385d7d719abd2ac1f1c +size 44648 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png index 82093102db..cb17901678 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:401e526ee8c3e27e90d34a9cb86561789172a9b91b5b5a6f5955aa4b001dfb1a -size 47658 +oid sha256:2e0782f2c450127e1079999e76c4683c0550e1c3fc6867fe6a8438c457a1c212 +size 43210 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png index d7778998b9..1ef1806d32 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:0f7fd6001307e447476310fc91cfe2eab0bddd8d0bc137a9a35698320f30f706 -size 50559 +oid sha256:ca9bd8eaae03a23e19f307e48200a3acad5adaf25f85b9ff996dfd34e2afa7ea +size 47683 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png index 777eaaf717..09d0b0b2dc 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:17c582d1666c5c7b6040b007a13f0c6367eccd07d2a35847e4751bf8e5de7ed6 -size 48691 +oid sha256:8ce03c0a8cd57000abb832adfc9109ce11901f2e7299ede7f0d1d26954d71dc0 +size 45588 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png index c59448b50d..8c96b6425f 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:673f1524b9a0e8284600cdcc5536bdf98574424c31a8a35552a75cbdd404424d -size 47490 +oid sha256:14e924fdb76d565050a354a7fbf423e2f2a42bf4ad97e4ea936ab774fd4c7817 +size 45027 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png index f0c70fc293..4bc445b2fa 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:a7aff67290ac05d786c65cdc2dabc4bc6dbf60a802d0b77ae8c8404fd05e1bb7 -size 49256 +oid sha256:b65295a9ef7403a39960379d005702837cdd994f95ef4ac68901052c28d3ea1f +size 45989 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png index 4c43697ac5..f47c6ad939 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:ec0e7d344810f5fa43b9bc05c41116501350e3daedabd33c97782082264369ab -size 49157 +oid sha256:29964bd165474c6d04de2030c5529af2e2413ee84f3aaf9bd317303a829fcd46 +size 45922 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_14_de.png index f0d89fc365..c52350d443 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:a012a170cbd17e1fe588c27e41e531baa12097830ed205ac5c9c51c5bd62e003 -size 50108 +oid sha256:9ecfbf047383b98642fadba05e9ce0fcf74126088ee690bb4c32be65588408ab +size 47473 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_15_de.png index 865602ac62..61cde4f5b1 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:4f734bad723be9fada8b38763321ea63778f0ebfd65567472432ac32634d7b67 -size 50695 +oid sha256:9ce8737bb8f4774165acf35250efcf9ca6f05158073ffe50601ffca66bcc60c7 +size 47907 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_16_de.png index 1a59d9ec17..d3a81f277b 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:7a3c70bad40927fcd45902da33a3fe12fa50057e278c6009b1ed5befe4512182 -size 49470 +oid sha256:592d316544feaafacec23fdcf5cdb4470a8207835ba87a723ef45b4b2f764917 +size 46236 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_17_de.png index 1c01af3450..dc25cda13d 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:a9e7f2edac607fcd372a991078a4d9d92e01e148ddebce2785174dbf877e14bf -size 49045 +oid sha256:88816d1cd342284e6af3da0e89236a03fdc149ebe7d621d8590d8730d8c81bcc +size 45758 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_18_de.png index aa64291503..805a1f7fc5 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:755919c37c18cac3fe595975847620ce4a0b836b01546fbbfd3cdd1693ad5435 -size 45945 +oid sha256:ce7df8bc9256fad4cb3cff8f3147f1c74ac31a9e379b86c428b31d8ee0a1b3ff +size 44438 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_19_de.png index 1cb9dd897b..94ba841c62 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:735d92a8a39f4ec22ab51830cc0a5877697d028a12ba4cec2c2cce0475cddea5 -size 45888 +oid sha256:df68b3579de58586a02141996672bb25cdf1056030a2341ae70383fbe2648f04 +size 44404 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png index 1d527ce0e6..c43dfc4765 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:7c9e86d87f93e3e96ee25b6090b0f989baf173a189a3c2274dc2cdd06dd566e1 -size 44223 +oid sha256:f036fb2f808035db5e5891f48c51586883f1cf52116c07be1698cb901bb12a28 +size 41932 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_20_de.png index c6e83dd003..11bc2637cd 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:1ed0a0e971a25574d13b6bbc5e0137f8a3f2bb1909d6149d16e09199dce3c25a -size 49535 +oid sha256:4fa2e5f4110ac0235e6e15ce5dd11b684a301e8508ef4a339f819239ed7bdf6a +size 46219 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_21_de.png index dd2ebef37e..cadcd1e2b4 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:fd5dcb36fc1e9cb675d461850e88f80402d30e43c6f8709ab3274326ad4a42b5 -size 49454 +oid sha256:a8f1c358b1de30937278423b42bdc468cd36a82ff78cb0c687cd26c9e8ce3b05 +size 46106 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_22_de.png index 9c0a4282b1..9ed59d7c04 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:b24f32d9a8640b7d1ef5a2b77d6198d3dbfabcab7a9cf46af8a0b24e23655eeb -size 48999 +oid sha256:3e4d19c67a12095c513363905af5f9d01b1f96ed21f8809b3a34162fb7f4cd18 +size 45707 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png index 24966de76f..784d22cc5e 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:e92a8647bc6be119cb6dd401267f3369ad0ffa61372efc21d8e1752350c37a54 -size 43617 +oid sha256:dc77c07d95d2fe8482d0aa1a16f02ad1f1bb12b42b96e49613c305bb7001d85b +size 39970 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png index 95c8bffb80..4e841931bb 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:39e4eab91f3d07daff1437991ec74005e7c79eed3f9b4ff71c154d68bbce3e71 -size 46400 +oid sha256:2f92411b4c22dd91cea3fe2b83e68855106f0cc8f8e98bff7120d7ad9ee08b8f +size 35510 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png index 5c5bc77cb2..b81166bff6 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:2ce073c24e1d2b0cfcc6fc64cc62042c5bb9260e7636441827fdc746964aefbe -size 48017 +oid sha256:dc47591d0c528d2690c93cc753944a3d14cf5874a76375e85280f20248e0a461 +size 44643 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png index 0a07cb8799..1ca8f85e28 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:19fa23046e80df108a9d9cabe2b34d713e12d74127b3c4a320b5770e286429ea -size 45506 +oid sha256:6b8c3af45a0302fbbb8162acb3cff65f3ece5d9c583655c73490ebfc4d67fbf6 +size 44110 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png index 71787b6f1f..b7df110e3c 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:eb164e2c9796a1d12a0146b3fb67d29813be54d42b0628d17a323a525b866177 -size 47065 +oid sha256:2e2ae44ab89b544cb00c4cc72bc20eb4b7c1ad8964bf6eaf95499cd4727ecb01 +size 36461 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png index a9ebd612f5..c3363c0921 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:6d764589c800027ef6f34660324d980c6525475b0a65d7f1e7b4afa4f4f19aaa -size 50155 +oid sha256:b626694fb5e69a36f345ff1500aa502fcc6b780d94af51670a43891702bbb0bf +size 46728 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png index 9a167dc9b8..1bda15b8d4 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:a3784f50878f750bc04b45ca657ccb89d74343164ef7b0aa06fb43260f46ef72 -size 49429 +oid sha256:e4d3dc8b6a30ec8a716af574f5f92d33cd1804eeb33427b162a439257157e694 +size 46229 diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png index b2f44c24d5..8764bb944e 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:91302b37b756a39cae34160110d2f7bb3fd67129343cde63ae22b109861299c7 -size 48671 +oid sha256:2dd181d7a3d507f25a7cb7717dd96a0a2e6f37896291ce160c356f8ab89b3637 +size 44769 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png index 5781478873..7fbe8f888d 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a78ad12392ea4d4e9e54ffbc786a6e3eec73294e088f5403db88af5269a6b003 -size 18523 +oid sha256:0578d53cd64a18056d550c9b0b8bdfbf510392b1898718efc6e753d251c21a2b +size 18557 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png index 819491fece..f45704af84 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dc117e18612891f202c6bc093e4a2063632ee974939606af6eb09ee36deba9e -size 22172 +oid sha256:0f63174a49edc5ce7f1e7d13253c1fc8449dfd3d3029a240d6d47ded6b7b20fb +size 22233 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png index ad63075b13..4fbd71efb8 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc3534d406c3ef468e9af1b86d8d09998fd173aa1f63627412053f4e4d76bbcd -size 27625 +oid sha256:bc661e4e10991f3810a28a69381036a8445fd44aeb27614a6ae59668c8ddcddf +size 27638 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png index d5df6b6df0..8c6d78c1d4 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1d10f6ddd38cffdf7f8ba188790f6115b18b95de401ce08ba4530f249f99410 -size 28109 +oid sha256:7c34192c1c65387e5772a9763cab97c12fffdbbc148c0d08343c211a3b8eafcb +size 28105 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png index 6d6878395b..5d00c96c9b 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0666fc8348e5f33b265d2345973aa529ca12e464535331c71244cf3a17c94cf -size 33570 +oid sha256:b3face6ab5952fafe521e1df418eb1ea14746107b78c993c992f188f4f0b5ee6 +size 33690 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png index 56c4a02de7..f23adb562c 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24bc5b3a5be02a4dabe2a4c1a773206dc7f991ea99e6b5d2c1b8e99f69055611 -size 31547 +oid sha256:290e8fd9f8b7ee1532d050dbfb0c888870c12f88e9c8c60a3de54b1caad3e453 +size 31655 diff --git a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_de.png b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_de.png index 7cc591adbc..c9f77b1c8c 100644 --- a/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_de.png +++ b/screenshots/de/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cedc1ead4632facdffa78ae83a0636d08e92013184587f346d13cbc4f98fd29 -size 35997 +oid sha256:19fe1682689050cce7831aa90f339175248ba578ac68fc7e1913b3eebece61f8 +size 36059 diff --git a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_de.png b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_de.png index 6224a55c6d..8478c19fda 100644 --- a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71815785672b31d061a25b3980ecb368c54d1e7259533fd1989819ae3c922e2a -size 74688 +oid sha256:e697b923e1e387be4bd8d3a8de0482c9be4a670192f29bb3c5529ff211e9e442 +size 71612 diff --git a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_de.png b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_de.png index 6224a55c6d..8478c19fda 100644 --- a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71815785672b31d061a25b3980ecb368c54d1e7259533fd1989819ae3c922e2a -size 74688 +oid sha256:e697b923e1e387be4bd8d3a8de0482c9be4a670192f29bb3c5529ff211e9e442 +size 71612 diff --git a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_de.png b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_de.png index 4147e93eef..9462258aaf 100644 --- a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a47734fb0be5195c915373f922ed2fb87f3d89c5c517fefd0fea00cad76bbca4 -size 75418 +oid sha256:4fb4462ae1261dc77869d984152bac7bd810c92a82782c36e2bcc448d3aa6459 +size 72336 diff --git a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_de.png b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_de.png index f8fa0ef3e5..73080a8c95 100644 --- a/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_de.png +++ b/screenshots/de/features.securebackup.impl.disable_SecureBackupDisableView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4c12cdd10cc2fb443c4dc248ab03740fd68cb8a2f8021225bee5010dd5e8438 -size 45465 +oid sha256:eb86dfe02f725547da2b90a4f9390db134f1f27e8c0253a33ae47ed9feb36cee +size 45289 diff --git a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png index 44eb24b2f7..186d5c4500 100644 --- a/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aa8259805201e40926ad24b4ac815c38ad03c06df4a931342d954704899366e -size 43769 +oid sha256:85c622ee5e7cc5377b07b9df0d6ed856551200af720101f084c56260d5024516 +size 43767 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png index 69691a898d..5db03f0da8 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ba5eceb0adc4b9feb3834f967dac81f1e2b6766934f6aadd54f3be011a98f6b -size 43156 +oid sha256:3644179c35846aec7a93ee45a6e0ee14ec1ca7795ca270a26cddc7c9813cedd3 +size 40448 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png index 69691a898d..5db03f0da8 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_10_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ba5eceb0adc4b9feb3834f967dac81f1e2b6766934f6aadd54f3be011a98f6b -size 43156 +oid sha256:3644179c35846aec7a93ee45a6e0ee14ec1ca7795ca270a26cddc7c9813cedd3 +size 40448 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_12_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png index 2add9f0e02..122e462156 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_13_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb7e2b92bd620115488d20e501f4040a042907dcf93d24fc77ce5a9fc178b83d -size 70404 +oid sha256:4978ac3b31f5f03fc81846e64c183a857d1f468bc1dc2816b123b92e5b2a1824 +size 67792 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 53994b7029..6d0b15b0f1 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:1293917283d4db5644f4cdbb00a76b0071dbbde7574b8ec3376f19a0fcf6dbb9 -size 75856 +oid sha256:552461f34b6873315ec01cc013fbc3ef4a0796c9bfa0bfd445ef94e7db2d3317 +size 73308 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png index 7a1dbd9583..6e24f8a5ad 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_15_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:587ae11b72d16310df68b70367a7c1dba8611dd252be29c9e403d7dfb216f43b -size 56498 +oid sha256:e2f75acc56ee1d7e6d0381db66cc8771f787e37c51171d1d0e478045759a8997 +size 54450 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png index 2f90072e8c..822271d7a7 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_16_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54130f6586c04b3eca992f3b3ab08772c3a8dc838800dc0847aaa30fad664021 -size 68285 +oid sha256:f1c9d7db00eb7b3eb03328c7e885e8ed608e8747851e3beab4c9f2d9613fb1b3 +size 65694 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png index 81fccd7025..f0e0b4d14e 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_17_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be67cbcb3279bb0d0849ee037d4f5310d7c8e56e87906838c0c0588da19ff6ce -size 52661 +oid sha256:23ded671d28f0bf1f6f857575603e4f4a63e666ce184aa1cb24d09e868f98bef +size 51149 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png index cdc5c1bd63..75a0e8a25a 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e065760677d14694ab6444762666f2d3af5fe8069cdbb08fdd9ddd3b5d53207 -size 44405 +oid sha256:9acf83c1389f316025d5aa95fe3ba43dc871df0cf01cdf364a228090c2873ff0 +size 41709 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_3_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_3_de.png index 0c892f4de1..08ee8d01ed 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_3_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7e3df55da07e3d081934df198f12cefed4599acc8eb8b3db25907cc5062e430 -size 45029 +oid sha256:17cc8bd2c275abd9f02a6ec753c05e8af0426c3831ed51d1caa3e3b034ec48eb +size 42318 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png index 69691a898d..5db03f0da8 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ba5eceb0adc4b9feb3834f967dac81f1e2b6766934f6aadd54f3be011a98f6b -size 43156 +oid sha256:3644179c35846aec7a93ee45a6e0ee14ec1ca7795ca270a26cddc7c9813cedd3 +size 40448 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png index 079249afcb..efef76cdd6 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_6_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba49338dbd77ebbfc82d555433e03c9ed222355ac8d716dac5b1677811e55e6a -size 41844 +oid sha256:5e39e5084b58380e734fa88b624e4715489bf01411997a35f4eebcfffa11edb1 +size 39552 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_7_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png index e20d4f0a54..391331f263 100644 --- a/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png +++ b/screenshots/de/features.securebackup.impl.root_SecureBackupRootView_Day_9_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b39a9cbacf1e86133a1031a105dec92a910a364f4758de3d48a8c63db232430 -size 44028 +oid sha256:a4b9d3c8cdbd10537eba77829cd3c91f18e6637ae1d02fcbc242cc26e860e226 +size 41319 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_de.png index fd27a4660d..700a209aba 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ff3fab68e7412f4007b2fbed67268f114064097e3256d051b235cee3350296 -size 50172 +oid sha256:4c89bb4bfe847f0a71ec7b74627b21f9e20245472d16ba413416eb3705d5859d +size 50721 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_de.png index 13bedfc6b3..742c8b1be9 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:deaffb7054cdb7dff2437f94f19bbb7a135e468d205fd87df9fd24a863483008 -size 46802 +oid sha256:785114ab0576fe9a69fcffa7dff1252ea660345a5706949e461878859dde0c33 +size 47362 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png index 1292c01a4f..894b66623c 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90ec71c1f37b2cadc100c3ad14fc03aa1abb76881834a97c9d7db9deef49456f -size 40043 +oid sha256:ada44308165eefb690684726100507bc2c20e1d363a3555f7d8975c6645e0536 +size 40144 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_de.png index d6f33a24d0..62f0a77598 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9212912117ff8653d8cc2c15eb360e2da0ef68e5ef79846f3e2f00f1fab9c8a -size 62526 +oid sha256:1ae69f5a8fc884a2ca02d8c84e9342b9633512de78348a0bb2d661d5735e708a +size 63185 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_de.png index 4e2a7b2671..3fc33a270d 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d61ffb76ac311f35f3b3b5e83810e62524efbca9721f35812d1ae2386482b1ee -size 59008 +oid sha256:6930558f6c4cda30d6a83fa320a7a17d4c45db0bfb2e9bf21b13701d047e76a2 +size 59611 diff --git a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png index b723e43390..388cb2f945 100644 --- a/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png +++ b/screenshots/de/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa77b425148e59b5d230f835baacc1ca3ac184a09e538057bf5d6438f20f83a4 -size 41503 +oid sha256:57dc90ddefeb2743ef616ddd11e5a9cb3847d714e2f452e2e58f7d7cc02c3422 +size 41780 diff --git a/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png b/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png index 34d3e5d494..e86f031a8e 100644 --- a/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png +++ b/screenshots/de/features.startchat.impl.components_SearchMultipleUsersResultItem_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:993191e7db24d10111a3b7c22c0e192e28e9a3e170edb57de14ec205de73cf15 -size 95383 +oid sha256:fa263cc9cfb7cc47dacfa4ea65a9f4e8660e1a17df61e357d5cf76928dd89100 +size 90714 diff --git a/screenshots/de/features.startchat.impl.components_SearchSingleUserResultItem_de.png b/screenshots/de/features.startchat.impl.components_SearchSingleUserResultItem_de.png index 8d1aac76c6..3eff580ee8 100644 --- a/screenshots/de/features.startchat.impl.components_SearchSingleUserResultItem_de.png +++ b/screenshots/de/features.startchat.impl.components_SearchSingleUserResultItem_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b60fdeb5bd6fbf7f3ab3eca81f2109b02101bec9a8c812ceb02ac8d19840e2 -size 51866 +oid sha256:caa936c590efb400eec2e3d238bec4e48b81883371057f34badf0abb1644554c +size 49654 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png index 502b87bbdb..1230b616fd 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7884a15f505913ba13dcd99320b1b9d26cbf1e8f15eb051e65d8fbedc1a828c -size 27673 +oid sha256:fa14bce431debf9d9a64c4a47bed9d89730fb21d480af186dc2ac53a9e52f1af +size 31785 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_1_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_1_de.png index 33479d694d..14b7d796bc 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_1_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cea006dcb900e70cc9f4be90b9144e340768e7d23d90932c99422eaf8ea96fb -size 21203 +oid sha256:0f4fe803d3318a31919099958d793ecb4f5eae7109596712de23c89e6c982ea4 +size 20145 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png index e4c6ad93ff..358883997e 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41ddc90c4f22013037b7786f2b91e35dd2db7329d43f12cb72f8cd1474e89d3c -size 31725 +oid sha256:a99cb9da94e1ea3050ccb1e9c32380fde1b5e55ea806c4c2aa5f7bc8c5d0b0fd +size 30750 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png index 3e3fcc2526..fc81f7c584 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a17fadc85a373556a338cd4c9ceaf35432b1853e06641237e6d3f4bf7cd0979 -size 51495 +oid sha256:af238ae63b675069702e65b52c1e4b272294aa0a2c971a3394a67535ee89e45c +size 51165 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png index 75e7c2f592..3bbed27e43 100644 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png +++ b/screenshots/de/features.startchat.impl.root_StartChatView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44855b0887ce9ccd5a0302c8d11ba6bf556badaefd33f897c5635d49cb44a99e -size 45071 +oid sha256:26cbf2c908b89e8cd2daa1a35cfd58dcea951e03a2ecc2334af7d8241d038254 +size 41339 diff --git a/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png b/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png deleted file mode 100644 index 1230b616fd..0000000000 --- a/screenshots/de/features.startchat.impl.root_StartChatView_Day_5_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa14bce431debf9d9a64c4a47bed9d89730fb21d480af186dc2ac53a9e52f1af -size 31785 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 e7254790cf..9f12c8c18d 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:f7bf1f5e039bdff2ba1da2678d57637564288733b9958042b86867350277ca9f -size 25025 +oid sha256:0c80ae6573c4d41b14827623890cb6b211783c708d7417c3b9f1e1ab29462098 +size 25198 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png index c7bfa31dae..81b5bb4a95 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_5_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a73316a4090908c2c8db15fa408e132f3fc7308c898311008f59ff62fdb94801 -size 23050 +oid sha256:09404876c06fa55c2e01f48380f7a4767d8c55f0cd46bdec78f4d7687ba8101d +size 23150 diff --git a/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png b/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png index ebf998ba32..5b11ec067d 100644 --- a/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png +++ b/screenshots/de/features.userprofile.shared_UserProfileView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa5d7bb68d549eab01750a043f0c844cf9ff2609e813fa6be5170f7e47da04f4 -size 36872 +oid sha256:35ac4bfa2e7bffba17448459c52098400f421fc3af55a7de16c24f3f293d0d42 +size 36464 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png index fc2d5b8229..4cfccb8015 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6d6cb69eb5d310ca7dae9127015fb3443325b16322c94958c4bf947fcc70514 -size 29438 +oid sha256:00d1fe85f79d7d25867287c50e50f2d0b37b52fe93c4cee70b17da485c912639 +size 30380 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_de.png index ce7583ad06..ce9e73fd68 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84fb184c49d26ad7636588885daf3c9169209975a5b0e2f1c9ffcea6a3875683 -size 43302 +oid sha256:70c06787bdb7614fb66fb9c16a9703fc572500cbd4b7ba48d806b6c6bb68b649 +size 44169 diff --git a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_de.png b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_de.png index 95995db201..5c5957c19b 100644 --- a/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_de.png +++ b/screenshots/de/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2087ce92d6d532a71a1dcc3f0824a34107316030e25ce58d46696675ddd6f264 -size 42907 +oid sha256:9bcad1fdaeb5596e17cddad4498d58208eccb7dcf38efdc7434bf82badffb5ea +size 43767 diff --git a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png index 275f38c3f5..5673458928 100644 --- a/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png +++ b/screenshots/de/features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2e955575444e16c332ab1e22cc121566a4f8744b8e0a01c69c5f3a9ce8ba033 -size 29507 +oid sha256:3ffba2cf2bdbed63ffd3f378a9e5b2a8c08bc316c500d7c782dbca8152c07224 +size 30449 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png index 030533ecdc..1b51febed4 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7a87b58fe4bc087197b3639fb2e31fd884c45b0fae9dc3e57657fe7d3299360 -size 31013 +oid sha256:913cc4e9cfc3b519dc0b8827a889da93b31b8520edc86f186cd760263cc27538 +size 30995 diff --git a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png index ba00c135ae..a50eac7fc1 100644 --- a/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png +++ b/screenshots/de/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77ec7e6ad8dc1a9030d09b259221360ee3cd838e512b93f2ff165ec1eab7ed49 +oid sha256:02d340df0a979691166b315aeb152c0249a90058865824aceeb2165ffd73de2c size 31446 diff --git a/screenshots/de/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_de.png b/screenshots/de/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_de.png new file mode 100644 index 0000000000..83302d6ba4 --- /dev/null +++ b/screenshots/de/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16dee1dfe746cd40dac9fe8f0af3ecb42cd14bab5cc490917b589beed065ed1b +size 14686 diff --git a/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png index 4263d29577..012c3b1574 100644 --- a/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff83b8d841c355dcac18d1ba0c7a66d0927e23be66bfabe8bc791e6f2ff3db22 -size 14008 +oid sha256:fbebdd9012c183c27ed79a3cb1dd5b823f73025756d9875c0c5a7af6f126dfa1 +size 14153 diff --git a/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png index 88e163b840..5fc8b4a8fe 100644 --- a/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CheckableUnresolvedUserRow_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4d6fb36f1340276b3f525505e8a51245f63a1fbe8637ad33c9c3c2ef793679d -size 114204 +oid sha256:81e2d991659061d40047373c3b3aedff5a4bc2b42c2b1355448b73784891b2f5 +size 109822 diff --git a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png index 74fca61a42..6b49ae7f1a 100644 --- a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9da4a0d12b26e2bf2bc3c6a0b032d462ccce782ee6ed5a62c7c32a7c2d4e0a2 -size 28400 +oid sha256:4484ff8dbba915c439270cbebab45c503d94dc82c7641a674153d88aa2aa3143 +size 28017 diff --git a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png index 6e9497454b..752973ddb4 100644 --- a/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png +++ b/screenshots/de/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d365fb5ebb274cdf830763e8354e5fb532929f590f9ae93259c823fa66a64a5 -size 39239 +oid sha256:402ca214ecc34d4a5a5faf7583a879ab26e6c64b86904034dfa050a27a7b7a2d +size 38789 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_de.png index 169e0d2b10..f89e8cf1c5 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:622e92307a1aae1a9feb420709f74d9cef063cdfd8e9f562e65655cb54ee804f -size 9991 +oid sha256:f02b4fe30d6ab57d618c925d3a110b5ca8f299717032b1380d91437acda62b7b +size 11633 diff --git a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_de.png b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_de.png index e2acbcc89c..6aee76799c 100644 --- a/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_de.png +++ b/screenshots/de/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ad1db5f211ed3df04d31e973297a82685168834ee6b629ce10c3b9e82c226c9 -size 10512 +oid sha256:1dde928940515398c089e258787afca156fd1dceb9b80559c339e94e97864f12 +size 12052 diff --git a/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png b/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png index 250826b00d..25ad5c2356 100644 --- a/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png +++ b/screenshots/de/libraries.matrix.ui.components_UnresolvedUserRow_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7179346d56f1c4772169e344f1e5f01b42a5cbb86093dc6e14728f970f5fc6b -size 72739 +oid sha256:76955fb8324747c81687727bbced594f00d2c2610d91474123ecd6ced14394a4 +size 70297 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png index 5fa5394165..748733038c 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e2325778fc5bdaa5ffd421f77cb83d1927ad4ad6731337b4878a99d3fbfe8aa -size 46053 +oid sha256:f02798a686487a372f781ba8db1c08364b4b7364e49ca6084eb48889967a329f +size 46360 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png index 5fa5394165..8ad903b190 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e2325778fc5bdaa5ffd421f77cb83d1927ad4ad6731337b4878a99d3fbfe8aa -size 46053 +oid sha256:555a9ecff384c0771f9825e5d222c6094818fddf79f144e4061d933bf752fd89 +size 44528 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png index 1e6c6d88eb..3e11375bf3 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f845e0783b6f09ef7b0472f22465e8e5b086102d711e468f1466df45ff928ed -size 50410 +oid sha256:de1d4d817742b8390b7e09bd8c66ca1ce294b22dc2c8c111205bec3436b57222 +size 51558 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png index 79f832dcc3..67b61583b1 100644 --- a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2917a353e78569a92d5d276e99ebfce334ad8848d488f58312abb9d7bcf654b -size 33327 +oid sha256:2bc723ee90bcd7038d78ccf738422daca8a0b0d54f9e86512399230912281d9a +size 33332 diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png new file mode 100644 index 0000000000..508383d519 --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d94152af559ac75475599fb5cf14defc96997e73e38600ff51c869903944f4 +size 45625 diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png index 1ec5f1ed66..5c0d5c66e5 100644 --- a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0321ed8731d4867c59c482a8e631291537556f6003659b9e0c2aa77d61883a69 -size 45973 +oid sha256:b64f60650f09cd069468a3860808bed51ae2d9e11f2dc0798965674a15e2e04e +size 46275 diff --git a/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_de.png new file mode 100644 index 0000000000..b45beedb7e --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0a4907f2ebf197b1cec1715ac6682d21b7b1d0a80eb5d05108f1971b648126f +size 12445 diff --git a/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_de.png b/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_de.png new file mode 100644 index 0000000000..9541983cee --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e0d9c3194ce7ac6096be5007d9e4d3b8e3fc6784c0389b1d14a288b48677304 +size 13776 diff --git a/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_de.png b/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_de.png new file mode 100644 index 0000000000..d1207e09a2 --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f42dca3f4cdeb760ea11c3edcb8751518e7e42a423ffbafaf12cc97d9cd4861 +size 14965 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png index c4822fa433..b81a3c6a5c 100644 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2c35cf93948bbc2c344bc678db1159e77769577054bee9ed168cd07ccea5de2 -size 28202 +oid sha256:73a6a48d04d86e9d5e9e546681863c32f651e6dc9af6d7f042e7e36b7fd98997 +size 28178 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_de.png new file mode 100644 index 0000000000..d8118a67f0 --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6298eeaf8442246d35fcca3b9e46755353d12bd9d6bfaa8a1e0447ee5d730e92 +size 198311 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_de.png new file mode 100644 index 0000000000..090a403022 --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ef79c13235d94887d873c42faa9b09c901efaeae7602336c81fcf7c62955ea5 +size 198916 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png index 16575bf2f5..25848ecda6 100644 --- a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5300a530f5afb27d55369a9fec9af46e4ceccfe3ae6926d7bff90a7b8cbaa02d -size 44430 +oid sha256:1a1cb205c91f54437af19cf47218a6a4b520c3d6763c36716d48a2902e9622b6 +size 44981 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_6_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_6_de.png new file mode 100644 index 0000000000..80d843ceda --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_6_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebf8c21f8967a2bce2ef74ac896497e6732f8bbab5c895bc3d33e760791ea570 +size 126353 diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_7_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_7_de.png new file mode 100644 index 0000000000..fdc521ed19 --- /dev/null +++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_7_de.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:783d90aa7ae58d39a2495c7728827f8ac3b91609b36a867a1dc6c49611ba886e +size 126735 diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 0bf3fbcf3b..ad325c4a2e 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -1,99 +1,108 @@ // 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",20567,], +["features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en",20623,], +["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20623,], ["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",20567,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20567,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20567,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20567,], -["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20567,], -["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20567,], -["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20567,], -["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20567,], -["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20567,], -["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20567,], -["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20567,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en",20623,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en",20623,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_3_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_3_en",20623,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_4_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_4_en",20623,], +["features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_5_en","features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_5_en",20623,], +["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20623,], +["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20623,], +["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20623,], +["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20623,], +["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20623,], +["features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en","features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en",20623,], ["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",20567,], -["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20567,], +["libraries.accountselect.impl_AccountSelectView_Day_0_en","libraries.accountselect.impl_AccountSelectView_Night_0_en",20623,], +["libraries.accountselect.impl_AccountSelectView_Day_1_en","libraries.accountselect.impl_AccountSelectView_Night_1_en",20623,], ["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",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20567,], +["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20623,], ["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",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20567,], -["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20567,], -["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20567,], -["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20567,], -["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20567,], -["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20567,], -["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en","",20570,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20567,], -["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20567,], -["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20567,], -["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20567,], +["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20623,], +["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20623,], +["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20623,], +["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20623,], +["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20623,], +["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_0_en","features.space.impl.addroom_AddRoomToSpaceView_Night_0_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_1_en","features.space.impl.addroom_AddRoomToSpaceView_Night_1_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_2_en","features.space.impl.addroom_AddRoomToSpaceView_Night_2_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_3_en","features.space.impl.addroom_AddRoomToSpaceView_Night_3_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_4_en","features.space.impl.addroom_AddRoomToSpaceView_Night_4_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_5_en","features.space.impl.addroom_AddRoomToSpaceView_Night_5_en",20623,], +["features.space.impl.addroom_AddRoomToSpaceView_Day_6_en","features.space.impl.addroom_AddRoomToSpaceView_Night_6_en",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en","",20623,], +["features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en","",20623,], +["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20623,], +["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20623,], ["libraries.designsystem.theme.components_AllIcons_Icons_en","",0,], -["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20567,], -["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20567,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20567,], -["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20567,], -["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20567,], +["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20623,], +["features.analytics.impl_AnalyticsOptInView_Day_1_en","features.analytics.impl_AnalyticsOptInView_Night_1_en",20623,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20623,], +["features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en",20623,], +["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20623,], ["libraries.designsystem.components_Announcement_Day_0_en","libraries.designsystem.components_Announcement_Night_0_en",0,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en",20570,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en",20570,], -["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en",20570,], -["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20567,], +["features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsPage_Night_0_en",20623,], +["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_0_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_0_en",20623,], +["features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Day_1_en","features.preferences.impl.developer.appsettings_AppDeveloperSettingsView_Night_1_en",20623,], +["services.apperror.api_AppErrorView_Day_0_en","services.apperror.api_AppErrorView_Night_0_en",20623,], ["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",20567,], +["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20623,], ["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",20567,], +["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20623,], ["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",20567,], +["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20623,], ["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",20567,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_en","",20623,], +["features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_en","",20623,], +["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20623,], ["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,], @@ -103,19 +112,20 @@ 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","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20567,], -["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20567,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en","",20623,], +["features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en","",20623,], ["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",20567,], +["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20623,], ["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,], @@ -145,22 +155,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",20567,], +["features.home.impl.components_BatteryOptimizationBanner_Day_0_en","features.home.impl.components_BatteryOptimizationBanner_Night_0_en",20623,], ["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",20567,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20567,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20567,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20567,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20567,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20567,], -["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20567,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20623,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20623,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20623,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20623,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20623,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20623,], +["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20623,], ["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,], -["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20567,], -["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20567,], -["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20567,], -["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20567,], -["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20567,], +["features.rageshake.impl.bugreport_BugReportViewDay_0_en","",20623,], +["features.rageshake.impl.bugreport_BugReportViewDay_1_en","",20623,], +["features.rageshake.impl.bugreport_BugReportViewDay_2_en","",20623,], +["features.rageshake.impl.bugreport_BugReportViewDay_3_en","",20623,], +["features.rageshake.impl.bugreport_BugReportViewDay_4_en","",20623,], ["features.rageshake.impl.bugreport_BugReportViewNight_0_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_1_en","",0,], ["features.rageshake.impl.bugreport_BugReportViewNight_2_en","",0,], @@ -171,140 +181,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",20567,], -["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20567,], +["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20623,], +["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",20623,], ["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",20567,], +["features.messages.impl.timeline.components_CallMenuItem_Day_6_en","features.messages.impl.timeline.components_CallMenuItem_Night_6_en",20623,], ["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",20567,], -["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20567,], -["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20567,], -["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20567,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20567,], -["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20567,], +["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20623,], +["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20623,], +["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20623,], +["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20623,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20623,], +["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_0_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_0_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en",20623,], ["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",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20567,], -["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20567,], -["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20567,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en",20623,], +["features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en","features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en",20623,], +["features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en","features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en",20623,], ["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",20567,], -["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20567,], -["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20567,], -["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20567,], -["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20567,], +["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20623,], +["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20623,], +["features.login.impl.changeserver_ChangeServerView_Day_3_en","features.login.impl.changeserver_ChangeServerView_Night_3_en",20623,], +["features.login.impl.changeserver_ChangeServerView_Day_4_en","features.login.impl.changeserver_ChangeServerView_Night_4_en",20623,], +["features.login.impl.changeserver_ChangeServerView_Day_5_en","features.login.impl.changeserver_ChangeServerView_Night_5_en",20623,], ["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,], -["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20567,], +["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20623,], ["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",20567,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20567,], -["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20567,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20567,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20567,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20567,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20567,], -["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20567,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en",20623,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en",20623,], +["features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en","features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en",20623,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_0_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_0_en",20623,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_1_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_1_en",20623,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_2_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_2_en",20623,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_3_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_3_en",20623,], +["features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en","features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en",20623,], ["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,], ["libraries.designsystem.components_ClickableLinkText_Text_en","",0,], +["features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en","features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en",20623,], ["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",20567,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20567,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20567,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20567,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20567,], -["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20567,], -["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20567,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20623,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20623,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_2_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_2_en",20623,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_3_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_3_en",20623,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_4_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_4_en",20623,], +["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_5_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_5_en",20623,], +["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20623,], ["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","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20567,], -["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20567,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20567,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20567,], -["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20567,], -["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20567,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en","",20623,], +["features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en","",20623,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20623,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20623,], +["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20623,], +["features.home.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.home.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20623,], ["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",20567,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20567,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20567,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20567,], -["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20567,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20567,], -["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20567,], -["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20567,], -["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20567,], -["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20567,], -["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20567,], -["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20567,], -["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20567,], -["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20567,], -["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20567,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20567,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20567,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20567,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20567,], -["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20567,], +["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20623,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20623,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20623,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20623,], +["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20623,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en",20623,], +["libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_1_en","libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_1_en",20623,], +["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20623,], +["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20623,], +["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20623,], +["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20623,], +["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20623,], +["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20623,], +["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20623,], +["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20623,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20623,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20623,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20623,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20623,], +["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20623,], ["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","",20567,], -["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20567,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20567,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20567,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20567,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20567,], -["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20567,], +["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20623,], +["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20623,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_0_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_0_en",20623,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_1_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_1_en",20623,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_2_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_2_en",20623,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_3_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_3_en",20623,], +["features.invite.impl.declineandblock_DeclineAndBlockView_Day_4_en","features.invite.impl.declineandblock_DeclineAndBlockView_Night_4_en",20623,], ["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",20567,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20567,], -["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20567,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20623,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20623,], +["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20623,], ["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",20567,], +["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20623,], ["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",20567,], -["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20567,], -["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20567,], -["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20567,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20567,], -["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20567,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20567,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20567,], -["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20567,], +["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20623,], +["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20623,], +["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20623,], +["features.licenses.impl.list_DependencyLicensesListView_Day_3_en","features.licenses.impl.list_DependencyLicensesListView_Night_3_en",20623,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_0_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_0_en",20623,], +["features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Day_1_en","features.linknewdevice.impl.screens.desktop_DesktopNoticeView_Night_1_en",20623,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20623,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20623,], +["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20623,], ["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,], @@ -313,26 +324,24 @@ export const screenshots = [ ["libraries.designsystem.theme.components_DialogWithVeryLongTitleAndIcon_Dialog_with_a_very_long_title_and_icon_Dialogs_en","",0,], ["libraries.designsystem.theme.components_DialogWithVeryLongTitle_Dialog_with_a_very_long_title_Dialogs_en","",0,], ["features.messages.impl.messagecomposer_DisabledComposerView_Day_0_en","features.messages.impl.messagecomposer_DisabledComposerView_Night_0_en",0,], -["libraries.designsystem.components.avatar_DmAvatarsRtl_Avatars_en","",0,], -["libraries.designsystem.components.avatar_DmAvatars_Avatars_en","",0,], ["libraries.designsystem.text_DpScale_0_75f__en","",0,], ["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",20567,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20567,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20567,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20567,], -["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20567,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20567,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20567,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20567,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20567,], -["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20567,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20567,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20567,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20567,], -["features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en",20570,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20623,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20623,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20623,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20623,], +["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20623,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_0_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_0_en",20623,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_1_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_1_en",20623,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_2_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_2_en",20623,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_3_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_3_en",20623,], +["features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Day_4_en","features.securityandprivacy.impl.editroomaddress_EditRoomAddressView_Night_4_en",20623,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20623,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_1_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_1_en",20623,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_2_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_2_en",20623,], +["features.preferences.impl.user.editprofile_EditUserProfileView_Day_3_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_3_en",20623,], ["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,], @@ -340,28 +349,29 @@ 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",20567,], -["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20567,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_0_en",20623,], +["features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Day_1_en","features.messages.impl.timeline.components.customreaction.picker_EmojiPicker_Night_1_en",20623,], ["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",20567,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20567,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20567,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20567,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20567,], -["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20567,], -["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20567,], -["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20567,], -["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20567,], -["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20567,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en",20623,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en",20623,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en",20623,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en",20623,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en",20623,], +["features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en","features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en",20623,], +["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20623,], +["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20623,], +["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_0_en","features.linknewdevice.impl.screens.error_ErrorView_Night_0_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_1_en","features.linknewdevice.impl.screens.error_ErrorView_Night_1_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_2_en","features.linknewdevice.impl.screens.error_ErrorView_Night_2_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_3_en","features.linknewdevice.impl.screens.error_ErrorView_Night_3_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_4_en","features.linknewdevice.impl.screens.error_ErrorView_Night_4_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_5_en","features.linknewdevice.impl.screens.error_ErrorView_Night_5_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_6_en","features.linknewdevice.impl.screens.error_ErrorView_Night_6_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_7_en","features.linknewdevice.impl.screens.error_ErrorView_Night_7_en",20623,], +["features.linknewdevice.impl.screens.error_ErrorView_Day_8_en","features.linknewdevice.impl.screens.error_ErrorView_Night_8_en",20623,], ["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,], @@ -381,52 +391,52 @@ export const screenshots = [ ["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",20567,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20567,], -["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20567,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20623,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20623,], +["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20623,], ["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",20567,], -["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20567,], +["features.forward.impl_ForwardMessagesView_Day_3_en","features.forward.impl_ForwardMessagesView_Night_3_en",20623,], +["features.home.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.home.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20623,], ["features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_0_en","features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_0_en",0,], -["features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en","features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en",20570,], +["features.announcement.impl.fullscreen_FullscreenAnnouncementView_Day_1_en","features.announcement.impl.fullscreen_FullscreenAnnouncementView_Night_1_en",20623,], ["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",20567,], -["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20567,], -["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20567,], -["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20567,], -["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20567,], +["features.home.impl.spaces_HomeSpacesView_Day_0_en","features.home.impl.spaces_HomeSpacesView_Night_0_en",20623,], +["features.home.impl.spaces_HomeSpacesView_Day_1_en","features.home.impl.spaces_HomeSpacesView_Night_1_en",20623,], +["features.home.impl.spaces_HomeSpacesView_Day_2_en","features.home.impl.spaces_HomeSpacesView_Night_2_en",20623,], +["features.home.impl.components_HomeTopBarMultiAccount_Day_0_en","features.home.impl.components_HomeTopBarMultiAccount_Night_0_en",20623,], +["features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en","features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en",20623,], ["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",20567,], -["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20567,], +["features.home.impl.components_HomeTopBarWithIndicator_Day_0_en","features.home.impl.components_HomeTopBarWithIndicator_Night_0_en",20623,], +["features.home.impl.components_HomeTopBar_Day_0_en","features.home.impl.components_HomeTopBar_Night_0_en",20623,], ["features.home.impl_HomeViewA11y_en","",0,], -["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20567,], -["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20567,], +["features.home.impl_HomeView_Day_0_en","features.home.impl_HomeView_Night_0_en",20623,], +["features.home.impl_HomeView_Day_10_en","features.home.impl_HomeView_Night_10_en",20623,], ["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",20567,], -["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20567,], -["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20567,], -["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20567,], -["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20567,], -["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20567,], -["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20567,], -["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20567,], -["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20567,], -["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20567,], -["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20567,], -["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20567,], -["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20567,], +["features.home.impl_HomeView_Day_13_en","features.home.impl_HomeView_Night_13_en",20623,], +["features.home.impl_HomeView_Day_14_en","features.home.impl_HomeView_Night_14_en",20623,], +["features.home.impl_HomeView_Day_15_en","features.home.impl_HomeView_Night_15_en",20623,], +["features.home.impl_HomeView_Day_16_en","features.home.impl_HomeView_Night_16_en",20623,], +["features.home.impl_HomeView_Day_1_en","features.home.impl_HomeView_Night_1_en",20623,], +["features.home.impl_HomeView_Day_2_en","features.home.impl_HomeView_Night_2_en",20623,], +["features.home.impl_HomeView_Day_3_en","features.home.impl_HomeView_Night_3_en",20623,], +["features.home.impl_HomeView_Day_4_en","features.home.impl_HomeView_Night_4_en",20623,], +["features.home.impl_HomeView_Day_5_en","features.home.impl_HomeView_Night_5_en",20623,], +["features.home.impl_HomeView_Day_6_en","features.home.impl_HomeView_Night_6_en",20623,], +["features.home.impl_HomeView_Day_7_en","features.home.impl_HomeView_Night_7_en",20623,], +["features.home.impl_HomeView_Day_8_en","features.home.impl_HomeView_Night_8_en",20623,], +["features.home.impl_HomeView_Day_9_en","features.home.impl_HomeView_Night_9_en",20623,], ["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,], +["libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en","libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en",20623,], ["libraries.designsystem.ruler_HorizontalRuler_Day_0_en","libraries.designsystem.ruler_HorizontalRuler_Night_0_en",0,], ["libraries.designsystem.theme.components_IconButton_Buttons_en","",0,], ["libraries.designsystem.theme.components_IconColorButton_Day_0_en","libraries.designsystem.theme.components_IconColorButton_Night_0_en",0,], @@ -434,12 +444,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.enterprise_Icon_en","",0,], ["appicon.element_Icon_en","",0,], +["appicon.enterprise_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",20567,], -["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20567,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20623,], +["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20623,], ["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,], @@ -447,119 +457,119 @@ 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",20567,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20623,], ["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",20567,], +["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20623,], ["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",20567,], -["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20567,], +["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20623,], +["features.call.impl.ui_IncomingCallScreen_Day_1_en","features.call.impl.ui_IncomingCallScreen_Night_1_en",20623,], ["features.verifysession.impl.incoming_IncomingVerificationViewA11y_en","",0,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20567,], -["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20567,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_10_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_10_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_11_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_11_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_12_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_12_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_13_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_13_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_8_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_8_en",20623,], +["features.verifysession.impl.incoming_IncomingVerificationView_Day_9_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_9_en",20623,], ["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",20567,], -["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20567,], -["features.invitepeople.impl_InvitePeopleView_Day_10_en","features.invitepeople.impl_InvitePeopleView_Night_10_en",20570,], +["features.call.impl.ui_InvalidAudioDeviceDialog_Day_0_en","features.call.impl.ui_InvalidAudioDeviceDialog_Night_0_en",20623,], +["features.invitepeople.impl_InvitePeopleView_Day_0_en","features.invitepeople.impl_InvitePeopleView_Night_0_en",20623,], +["features.invitepeople.impl_InvitePeopleView_Day_10_en","features.invitepeople.impl_InvitePeopleView_Night_10_en",20623,], ["features.invitepeople.impl_InvitePeopleView_Day_11_en","features.invitepeople.impl_InvitePeopleView_Night_11_en",0,], -["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20567,], +["features.invitepeople.impl_InvitePeopleView_Day_1_en","features.invitepeople.impl_InvitePeopleView_Night_1_en",20623,], ["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",20567,], -["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20567,], -["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20567,], -["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20567,], +["features.invitepeople.impl_InvitePeopleView_Day_4_en","features.invitepeople.impl_InvitePeopleView_Night_4_en",20623,], +["features.invitepeople.impl_InvitePeopleView_Day_5_en","features.invitepeople.impl_InvitePeopleView_Night_5_en",20623,], +["features.invitepeople.impl_InvitePeopleView_Day_6_en","features.invitepeople.impl_InvitePeopleView_Night_6_en",20623,], +["features.invitepeople.impl_InvitePeopleView_Day_7_en","features.invitepeople.impl_InvitePeopleView_Night_7_en",20623,], ["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",20567,], -["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20567,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20567,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20567,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20567,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20567,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20567,], -["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20567,], +["features.invitepeople.impl_InvitePeopleView_Day_9_en","features.invitepeople.impl_InvitePeopleView_Night_9_en",20623,], +["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20623,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_0_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_0_en",20623,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_1_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_1_en",20623,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_2_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_2_en",20623,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_3_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_3_en",20623,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_4_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_4_en",20623,], +["features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Day_5_en","features.startchat.impl.joinbyaddress_JoinRoomByAddressView_Night_5_en",20623,], ["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",20567,], -["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20567,], -["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20567,], -["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20567,], -["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20567,], +["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_13_en","features.joinroom.impl_JoinRoomView_Night_13_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_14_en","features.joinroom.impl_JoinRoomView_Night_14_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_15_en","features.joinroom.impl_JoinRoomView_Night_15_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_16_en","features.joinroom.impl_JoinRoomView_Night_16_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20623,], +["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20623,], +["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_10_en","features.knockrequests.impl.list_KnockRequestsListView_Night_10_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20623,], +["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20623,], ["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,], -["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20567,], -["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20567,], +["features.preferences.impl.labs_LabsView_Day_0_en","features.preferences.impl.labs_LabsView_Night_0_en",20623,], +["features.preferences.impl.labs_LabsView_Day_1_en","features.preferences.impl.labs_LabsView_Night_1_en",20623,], ["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",20567,], -["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20567,], -["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20567,], -["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20567,], -["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20567,], -["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20567,], -["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20567,], -["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20567,], +["features.leaveroom.impl_LeaveRoomView_Day_1_en","features.leaveroom.impl_LeaveRoomView_Night_1_en",20623,], +["features.leaveroom.impl_LeaveRoomView_Day_2_en","features.leaveroom.impl_LeaveRoomView_Night_2_en",20623,], +["features.leaveroom.impl_LeaveRoomView_Day_3_en","features.leaveroom.impl_LeaveRoomView_Night_3_en",20623,], +["features.leaveroom.impl_LeaveRoomView_Day_4_en","features.leaveroom.impl_LeaveRoomView_Night_4_en",20623,], +["features.leaveroom.impl_LeaveRoomView_Day_5_en","features.leaveroom.impl_LeaveRoomView_Night_5_en",20623,], +["features.leaveroom.impl_LeaveRoomView_Day_6_en","features.leaveroom.impl_LeaveRoomView_Night_6_en",20623,], +["features.leaveroom.impl_LeaveRoomView_Day_7_en","features.leaveroom.impl_LeaveRoomView_Night_7_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_0_en","features.space.impl.leave_LeaveSpaceView_Night_0_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_10_en","features.space.impl.leave_LeaveSpaceView_Night_10_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_1_en","features.space.impl.leave_LeaveSpaceView_Night_1_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_2_en","features.space.impl.leave_LeaveSpaceView_Night_2_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_3_en","features.space.impl.leave_LeaveSpaceView_Night_3_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_4_en","features.space.impl.leave_LeaveSpaceView_Night_4_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_5_en","features.space.impl.leave_LeaveSpaceView_Night_5_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_6_en","features.space.impl.leave_LeaveSpaceView_Night_6_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_7_en","features.space.impl.leave_LeaveSpaceView_Night_7_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_8_en","features.space.impl.leave_LeaveSpaceView_Night_8_en",20623,], +["features.space.impl.leave_LeaveSpaceView_Day_9_en","features.space.impl.leave_LeaveSpaceView_Night_9_en",20623,], ["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",20567,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20567,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20567,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20567,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20567,], -["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20567,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en",20623,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en",20623,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_2_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_2_en",20623,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en",20623,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en",20623,], +["features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en","features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en",20623,], ["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",20567,], +["features.messages.impl.link_LinkView_Day_1_en","features.messages.impl.link_LinkView_Night_1_en",20623,], ["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,], @@ -612,47 +622,48 @@ export const screenshots = [ ["libraries.designsystem.theme.components_ListSupportingTextLargePadding_List_supporting_text_-_large_padding_List_sections_en","",0,], ["libraries.designsystem.theme.components_ListSupportingTextNoPadding_List_supporting_text_-_no_padding_List_sections_en","",0,], ["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en","",0,], +["features.location.api_LiveLocationSharingBanner_Day_0_en","features.location.api_LiveLocationSharingBanner_Night_0_en",20623,], ["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",20567,], +["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20623,], ["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",20567,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20567,], -["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20567,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20623,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20623,], +["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20623,], ["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",20567,], -["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20567,], -["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20567,], -["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20567,], -["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20567,], -["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20567,], -["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20567,], -["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20567,], -["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20567,], -["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20567,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20567,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20567,], -["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20567,], -["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en",20570,], -["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en",20570,], -["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20567,], -["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20567,], -["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20567,], -["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20567,], -["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20567,], -["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20567,], -["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20567,], -["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20567,], -["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20567,], -["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20567,], -["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20567,], -["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20567,], +["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20623,], +["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20623,], +["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20623,], +["features.login.impl.login_LoginModeView_Day_0_en","features.login.impl.login_LoginModeView_Night_0_en",20623,], +["features.login.impl.login_LoginModeView_Day_1_en","features.login.impl.login_LoginModeView_Night_1_en",20623,], +["features.login.impl.login_LoginModeView_Day_2_en","features.login.impl.login_LoginModeView_Night_2_en",20623,], +["features.login.impl.login_LoginModeView_Day_3_en","features.login.impl.login_LoginModeView_Night_3_en",20623,], +["features.login.impl.login_LoginModeView_Day_4_en","features.login.impl.login_LoginModeView_Night_4_en",20623,], +["features.login.impl.login_LoginModeView_Day_5_en","features.login.impl.login_LoginModeView_Night_5_en",20623,], +["features.login.impl.login_LoginModeView_Day_6_en","features.login.impl.login_LoginModeView_Night_6_en",20623,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20623,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20623,], +["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20623,], +["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_0_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_0_en",20623,], +["features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Day_1_en","features.login.impl.screens.classic.loginwithclassic_LoginWithClassicView_Night_1_en",20623,], +["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20623,], +["features.logout.impl_LogoutView_Day_10_en","features.logout.impl_LogoutView_Night_10_en",20623,], +["features.logout.impl_LogoutView_Day_11_en","features.logout.impl_LogoutView_Night_11_en",20623,], +["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20623,], +["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20623,], +["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20623,], +["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20623,], +["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20623,], +["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20623,], +["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20623,], +["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20623,], +["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20623,], ["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20567,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20567,], -["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20567,], -["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20567,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_0_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_0_en",20623,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_1_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_1_en",20623,], +["features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Day_2_en","features.securityandprivacy.impl.manageauthorizedspaces_ManageAuthorizedSpacesView_Night_2_en",20623,], +["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20623,], ["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,], @@ -665,26 +676,29 @@ 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",20567,], -["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en",20570,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20567,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en",20570,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en",20570,], -["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en",20570,], -["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",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20567,], -["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20567,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20623,], +["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_1_en",20623,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20623,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en",20623,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en",20623,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en",20623,], +["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en",20623,], +["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",20623,], +["libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en",20623,], +["libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_2_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_11_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_11_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_12_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_12_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20623,], +["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20623,], ["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,], @@ -692,38 +706,48 @@ 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_MediaViewerViewLandscape_0_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_10_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en","",20570,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en","",20570,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_11_en","",20623,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_12_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en","",20570,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_14_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_17_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_18_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_19_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en","",20570,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_20_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_21_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_22_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_2_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_5_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en","",20623,], +["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_7_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_9_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","",20567,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20567,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20623,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20567,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_14_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_15_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_16_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_17_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_18_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_19_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_1_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20567,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_20_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_21_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_22_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_6_en","",0,], -["libraries.mediaviewer.impl.viewer_MediaViewerView_7_en","",0,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_6_en","",20623,], +["libraries.mediaviewer.impl.viewer_MediaViewerView_7_en","",20623,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_8_en","",0,], ["libraries.mediaviewer.impl.viewer_MediaViewerView_9_en","",0,], ["libraries.designsystem.theme.components_MediumTopAppBar_App_Bars_en","",0,], @@ -732,7 +756,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",20567,], +["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20623,], ["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,], @@ -741,7 +765,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",20567,], +["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20623,], ["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,], @@ -750,23 +774,24 @@ 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",20567,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20567,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20567,], -["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20567,], -["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20567,], -["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20567,], -["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20567,], -["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20567,], -["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20567,], -["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20567,], -["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20567,], -["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20567,], -["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20567,], -["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20567,], -["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20567,], +["features.messages.impl.topbars_MessagesViewTopBar_Day_0_en","features.messages.impl.topbars_MessagesViewTopBar_Night_0_en",20623,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20623,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20623,], +["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20623,], +["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20623,], +["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20623,], +["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20623,], +["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20623,], +["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20623,], +["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20623,], +["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20623,], +["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20623,], +["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20623,], +["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20623,], +["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20623,], +["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20623,], ["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",20567,], +["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20623,], ["features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Day_0_en","features.login.impl.screens.classic.missingkeybackup_MissingKeyBackupView_Night_0_en",0,], ["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,], ["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,], @@ -777,113 +802,124 @@ 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",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20567,], -["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20567,], -["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20567,], +["features.home.impl.components_NewNotificationSoundBanner_Day_0_en","features.home.impl.components_NewNotificationSoundBanner_Night_0_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_13_en","features.preferences.impl.notifications_NotificationSettingsView_Night_13_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_14_en","features.preferences.impl.notifications_NotificationSettingsView_Night_14_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_15_en","features.preferences.impl.notifications_NotificationSettingsView_Night_15_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_16_en","features.preferences.impl.notifications_NotificationSettingsView_Night_16_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_17_en","features.preferences.impl.notifications_NotificationSettingsView_Night_17_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_18_en","features.preferences.impl.notifications_NotificationSettingsView_Night_18_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_19_en","features.preferences.impl.notifications_NotificationSettingsView_Night_19_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_20_en","features.preferences.impl.notifications_NotificationSettingsView_Night_20_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20623,], +["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20623,], +["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20623,], ["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",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20567,], -["features.login.impl.screens.onboarding_OnBoardingView_Day_8_en","features.login.impl.screens.onboarding_OnBoardingView_Night_8_en",20570,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_0_en","features.login.impl.screens.onboarding_OnBoardingView_Night_0_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_1_en","features.login.impl.screens.onboarding_OnBoardingView_Night_1_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_2_en","features.login.impl.screens.onboarding_OnBoardingView_Night_2_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_3_en","features.login.impl.screens.onboarding_OnBoardingView_Night_3_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_4_en","features.login.impl.screens.onboarding_OnBoardingView_Night_4_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_5_en","features.login.impl.screens.onboarding_OnBoardingView_Night_5_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_6_en","features.login.impl.screens.onboarding_OnBoardingView_Night_6_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_7_en","features.login.impl.screens.onboarding_OnBoardingView_Night_7_en",20623,], +["features.login.impl.screens.onboarding_OnBoardingView_Day_8_en","features.login.impl.screens.onboarding_OnBoardingView_Night_8_en",20623,], ["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",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20567,], +["libraries.matrix.ui.components_OrganizationHeader_Day_0_en","libraries.matrix.ui.components_OrganizationHeader_Night_0_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_0_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_0_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_10_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_10_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_11_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_11_en",20623,], ["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",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20567,], -["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20567,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_1_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_1_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_2_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_2_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_3_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_3_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_4_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_4_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_5_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_5_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_6_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_6_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_7_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_7_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_8_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_8_en",20623,], +["features.verifysession.impl.outgoing_OutgoingVerificationView_Day_9_en","features.verifysession.impl.outgoing_OutgoingVerificationView_Night_9_en",20623,], ["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",20567,], -["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20567,], -["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20567,], -["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20567,], -["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20567,], -["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20567,], +["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20623,], +["features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Day_0_en","features.rolesandpermissions.impl.roles_PendingMemberRowWithLongName_Night_0_en",20623,], +["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20623,], +["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20623,], +["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20623,], +["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20623,], ["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_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",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20567,], -["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20567,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_8_en","features.lockscreen.impl.unlock_PinUnlockView_Night_8_en",20623,], +["features.lockscreen.impl.unlock_PinUnlockView_Day_9_en","features.lockscreen.impl.unlock_PinUnlockView_Night_9_en",20623,], ["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",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20567,], -["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20567,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20567,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20567,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20567,], -["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20567,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20623,], +["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20623,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20623,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20623,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20623,], +["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20623,], ["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",20567,], -["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20567,], -["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20567,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20567,], -["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20567,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20623,], +["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20623,], +["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20623,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20623,], +["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20623,], ["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",20567,], -["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20567,], -["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20567,], -["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20567,], -["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20567,], -["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20567,], -["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20567,], -["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20567,], -["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20567,], -["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20567,], -["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20567,], +["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20623,], +["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20623,], +["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20623,], +["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20623,], +["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20623,], +["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20623,], +["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20623,], +["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20623,], +["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20623,], +["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20623,], +["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20623,], ["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,], @@ -897,223 +933,227 @@ 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","",20567,], -["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20567,], -["features.preferences.impl.root_PreferencesRootViewDark_2_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewDark_3_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewDark_4_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewDark_5_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20567,], -["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20567,], -["features.preferences.impl.root_PreferencesRootViewLight_2_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewLight_3_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewLight_4_en","",20570,], -["features.preferences.impl.root_PreferencesRootViewLight_5_en","",20570,], +["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewDark_2_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewDark_3_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewDark_4_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewDark_5_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewLight_2_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewLight_3_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewLight_4_en","",20623,], +["features.preferences.impl.root_PreferencesRootViewLight_5_en","",20623,], ["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","",20567,], -["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20567,], +["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20623,], +["libraries.designsystem.components_ProgressDialogWithContent_Day_0_en","libraries.designsystem.components_ProgressDialogWithContent_Night_0_en",20623,], ["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",20567,], -["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20567,], -["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20567,], -["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20567,], -["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20567,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20567,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20567,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20567,], -["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20567,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20567,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20567,], -["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20567,], -["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20567,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20567,], -["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20567,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20567,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20567,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20567,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20567,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20567,], -["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20567,], +["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20623,], +["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20623,], +["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20623,], +["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20623,], +["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20623,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_0_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_0_en",20623,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en",20623,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_2_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_2_en",20623,], +["libraries.troubleshoot.impl.history_PushHistoryView_Day_3_en","libraries.troubleshoot.impl.history_PushHistoryView_Night_3_en",20623,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20623,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20623,], +["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20623,], +["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20623,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20623,], +["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20623,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20623,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20623,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20623,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20623,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_4_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_4_en",20623,], +["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_5_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_5_en",20623,], ["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",20567,], -["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20567,], +["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20623,], +["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20623,], ["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",20567,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20567,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20567,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20567,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20567,], -["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20567,], -["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20567,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20623,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20623,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20623,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20623,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20623,], +["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_14_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_14_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20623,], +["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20623,], ["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,], +["features.location.api_RenderingMapsNotSupportedDialog_Day_0_en","features.location.api_RenderingMapsNotSupportedDialog_Night_0_en",20623,], ["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",20567,], -["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20567,], -["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20567,], -["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20567,], -["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20567,], -["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20567,], -["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20567,], -["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20567,], -["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20567,], -["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20567,], -["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20567,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20567,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20567,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20567,], -["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20567,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20567,], -["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20567,], +["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20623,], +["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20623,], +["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20623,], +["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20623,], +["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20623,], +["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20623,], +["features.reportroom.impl_ReportRoomView_Day_0_en","features.reportroom.impl_ReportRoomView_Night_0_en",20623,], +["features.reportroom.impl_ReportRoomView_Day_1_en","features.reportroom.impl_ReportRoomView_Night_1_en",20623,], +["features.reportroom.impl_ReportRoomView_Day_2_en","features.reportroom.impl_ReportRoomView_Night_2_en",20623,], +["features.reportroom.impl_ReportRoomView_Day_3_en","features.reportroom.impl_ReportRoomView_Night_3_en",20623,], +["features.reportroom.impl_ReportRoomView_Day_4_en","features.reportroom.impl_ReportRoomView_Night_4_en",20623,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20623,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20623,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20623,], +["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20623,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20623,], +["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20623,], ["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",20567,], -["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20567,], -["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20567,], -["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20567,], -["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20567,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20623,], +["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20623,], +["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20623,], +["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en",20623,], +["features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en","features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en",20623,], ["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",20567,], -["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20567,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",20623,], +["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20623,], ["features.roomdetails.impl_RoomDetailsA11y_en","",0,], -["features.roomdetails.impl_RoomDetailsDark_0_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_10_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_11_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_12_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_13_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_14_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_15_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_16_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_17_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_18_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_19_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_1_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_20_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_21_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_22_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_2_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_3_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_4_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_5_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_6_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_7_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_8_en","",20567,], -["features.roomdetails.impl_RoomDetailsDark_9_en","",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20567,], -["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20567,], -["features.roomdetails.impl_RoomDetails_0_en","",20567,], -["features.roomdetails.impl_RoomDetails_10_en","",20567,], -["features.roomdetails.impl_RoomDetails_11_en","",20567,], -["features.roomdetails.impl_RoomDetails_12_en","",20567,], -["features.roomdetails.impl_RoomDetails_13_en","",20567,], -["features.roomdetails.impl_RoomDetails_14_en","",20567,], -["features.roomdetails.impl_RoomDetails_15_en","",20567,], -["features.roomdetails.impl_RoomDetails_16_en","",20567,], -["features.roomdetails.impl_RoomDetails_17_en","",20567,], -["features.roomdetails.impl_RoomDetails_18_en","",20567,], -["features.roomdetails.impl_RoomDetails_19_en","",20567,], -["features.roomdetails.impl_RoomDetails_1_en","",20567,], -["features.roomdetails.impl_RoomDetails_20_en","",20567,], -["features.roomdetails.impl_RoomDetails_21_en","",20567,], -["features.roomdetails.impl_RoomDetails_22_en","",20567,], -["features.roomdetails.impl_RoomDetails_2_en","",20567,], -["features.roomdetails.impl_RoomDetails_3_en","",20567,], -["features.roomdetails.impl_RoomDetails_4_en","",20567,], -["features.roomdetails.impl_RoomDetails_5_en","",20567,], -["features.roomdetails.impl_RoomDetails_6_en","",20567,], -["features.roomdetails.impl_RoomDetails_7_en","",20567,], -["features.roomdetails.impl_RoomDetails_8_en","",20567,], -["features.roomdetails.impl_RoomDetails_9_en","",20567,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20567,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20567,], -["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20567,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20567,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20567,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20567,], -["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20567,], -["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20567,], -["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20567,], +["features.roomdetails.impl_RoomDetailsDark_0_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_10_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_11_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_12_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_13_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_14_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_15_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_16_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_17_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_18_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_19_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_1_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_20_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_21_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_22_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_2_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_3_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_4_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_5_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_6_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_7_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_8_en","",20623,], +["features.roomdetails.impl_RoomDetailsDark_9_en","",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_0_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_0_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_1_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_1_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_2_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_2_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_3_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_3_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_4_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_4_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_5_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_5_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_6_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_6_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_7_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_7_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_8_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_8_en",20623,], +["features.roomdetailsedit.impl_RoomDetailsEditView_Day_9_en","features.roomdetailsedit.impl_RoomDetailsEditView_Night_9_en",20623,], +["features.roomdetails.impl_RoomDetails_0_en","",20623,], +["features.roomdetails.impl_RoomDetails_10_en","",20623,], +["features.roomdetails.impl_RoomDetails_11_en","",20623,], +["features.roomdetails.impl_RoomDetails_12_en","",20623,], +["features.roomdetails.impl_RoomDetails_13_en","",20623,], +["features.roomdetails.impl_RoomDetails_14_en","",20623,], +["features.roomdetails.impl_RoomDetails_15_en","",20623,], +["features.roomdetails.impl_RoomDetails_16_en","",20623,], +["features.roomdetails.impl_RoomDetails_17_en","",20623,], +["features.roomdetails.impl_RoomDetails_18_en","",20623,], +["features.roomdetails.impl_RoomDetails_19_en","",20623,], +["features.roomdetails.impl_RoomDetails_1_en","",20623,], +["features.roomdetails.impl_RoomDetails_20_en","",20623,], +["features.roomdetails.impl_RoomDetails_21_en","",20623,], +["features.roomdetails.impl_RoomDetails_22_en","",20623,], +["features.roomdetails.impl_RoomDetails_2_en","",20623,], +["features.roomdetails.impl_RoomDetails_3_en","",20623,], +["features.roomdetails.impl_RoomDetails_4_en","",20623,], +["features.roomdetails.impl_RoomDetails_5_en","",20623,], +["features.roomdetails.impl_RoomDetails_6_en","",20623,], +["features.roomdetails.impl_RoomDetails_7_en","",20623,], +["features.roomdetails.impl_RoomDetails_8_en","",20623,], +["features.roomdetails.impl_RoomDetails_9_en","",20623,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20623,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20623,], +["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20623,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20623,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20623,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20623,], +["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20623,], +["features.home.impl.components_RoomListContentView_Day_0_en","features.home.impl.components_RoomListContentView_Night_0_en",20623,], +["features.home.impl.components_RoomListContentView_Day_1_en","features.home.impl.components_RoomListContentView_Night_1_en",20623,], ["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",20567,], -["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20567,], -["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20567,], -["features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenuContent_Night_0_en",20567,], -["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20567,], -["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20567,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_0_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_0_en",20567,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_1_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_1_en",20567,], -["features.home.impl.roomlist_RoomListModalBottomSheetContent_Day_2_en","features.home.impl.roomlist_RoomListModalBottomSheetContent_Night_2_en",20567,], +["features.home.impl.components_RoomListContentView_Day_3_en","features.home.impl.components_RoomListContentView_Night_3_en",20623,], +["features.home.impl.components_RoomListContentView_Day_4_en","features.home.impl.components_RoomListContentView_Night_4_en",20623,], +["features.home.impl.components_RoomListContentView_Day_5_en","features.home.impl.components_RoomListContentView_Night_5_en",20623,], +["features.home.impl.roomlist_RoomListContextMenu_Day_0_en","features.home.impl.roomlist_RoomListContextMenu_Night_0_en",20623,], +["features.home.impl.roomlist_RoomListContextMenu_Day_1_en","features.home.impl.roomlist_RoomListContextMenu_Night_1_en",20623,], +["features.home.impl.roomlist_RoomListContextMenu_Day_2_en","features.home.impl.roomlist_RoomListContextMenu_Night_2_en",20623,], +["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_0_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_0_en",20623,], +["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_1_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_1_en",20623,], +["features.home.impl.roomlist_RoomListDeclineInviteMenu_Day_2_en","features.home.impl.roomlist_RoomListDeclineInviteMenu_Night_2_en",20623,], +["features.home.impl.filters_RoomListFiltersView_Day_0_en","features.home.impl.filters_RoomListFiltersView_Night_0_en",20623,], +["features.home.impl.filters_RoomListFiltersView_Day_1_en","features.home.impl.filters_RoomListFiltersView_Night_1_en",20623,], ["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",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20567,], -["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20567,], -["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20567,], +["features.home.impl.search_RoomListSearchContent_Day_1_en","features.home.impl.search_RoomListSearchContent_Night_1_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",20623,], +["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en",20623,], +["features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en","features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en",20623,], ["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",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20567,], -["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20567,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20623,], +["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20623,], ["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",20567,], -["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20567,], -["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20567,], -["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20567,], -["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20567,], -["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20567,], +["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20623,], +["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20623,], +["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20623,], +["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20623,], +["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20623,], +["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20623,], +["libraries.roomselect.impl_RoomSelectView_Day_6_en","libraries.roomselect.impl_RoomSelectView_Night_6_en",0,], ["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,], @@ -1136,16 +1176,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",20567,], -["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20567,], -["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20567,], -["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20567,], -["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20567,], -["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20567,], -["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20567,], -["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20567,], +["features.home.impl.components_RoomSummaryRow_Day_29_en","features.home.impl.components_RoomSummaryRow_Night_29_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_2_en","features.home.impl.components_RoomSummaryRow_Night_2_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_30_en","features.home.impl.components_RoomSummaryRow_Night_30_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_31_en","features.home.impl.components_RoomSummaryRow_Night_31_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_32_en","features.home.impl.components_RoomSummaryRow_Night_32_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_33_en","features.home.impl.components_RoomSummaryRow_Night_33_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_34_en","features.home.impl.components_RoomSummaryRow_Night_34_en",20623,], +["features.home.impl.components_RoomSummaryRow_Day_35_en","features.home.impl.components_RoomSummaryRow_Night_35_en",20623,], ["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",20567,], +["features.home.impl.components_RoomSummaryRow_Day_37_en","features.home.impl.components_RoomSummaryRow_Night_37_en",20623,], ["features.home.impl.components_RoomSummaryRow_Day_38_en","features.home.impl.components_RoomSummaryRow_Night_38_en",0,], ["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,], @@ -1154,119 +1194,119 @@ 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",20567,], +["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20623,], ["features.login.impl.screens.classic.root_RootView_Day_0_en","features.login.impl.screens.classic.root_RootView_Night_0_en",0,], -["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20567,], -["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20567,], +["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20623,], +["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20623,], ["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",20567,], -["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20567,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20567,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20567,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20567,], -["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20567,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20567,], -["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20567,], +["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20623,], +["libraries.designsystem.components.dialogs_SaveChangesDialog_Day_0_en","libraries.designsystem.components.dialogs_SaveChangesDialog_Night_0_en",20623,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_0_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_0_en",20623,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_1_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_1_en",20623,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_2_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_2_en",20623,], +["features.linknewdevice.impl.screens.scan_ScanQrCodeView_Day_3_en","features.linknewdevice.impl.screens.scan_ScanQrCodeView_Night_3_en",20623,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20623,], +["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20623,], ["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","",20567,], +["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20623,], ["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","",20567,], -["features.startchat.impl.components_SearchSingleUserResultItem_en","",20567,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20567,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20567,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20567,], -["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20567,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20567,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20567,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20567,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20567,], -["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20567,], -["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20567,], -["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20567,], -["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20567,], -["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20567,], +["features.startchat.impl.components_SearchMultipleUsersResultItem_en","",20623,], +["features.startchat.impl.components_SearchSingleUserResultItem_en","",20623,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20623,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20623,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20623,], +["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20623,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20623,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20623,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20623,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20623,], +["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_4_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_4_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20623,], +["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20623,], +["features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_0_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_10_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_11_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_12_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_13_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_14_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_15_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_16_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_17_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_18_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_19_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_1_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_20_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_21_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_22_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_23_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_2_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_3_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_4_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_5_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_6_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_7_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_8_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewDark_9_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_0_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_10_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_11_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_12_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_13_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_14_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_15_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_16_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_17_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_18_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_19_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_1_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_20_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_21_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_22_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_23_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_2_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_3_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_4_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_5_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_6_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_7_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_8_en","",20623,], +["features.securityandprivacy.impl.root_SecurityAndPrivacyViewLight_9_en","",20623,], +["features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Day_0_en","features.createroom.impl.configureroom_SelectParentSpaceBottomSheet_Night_0_en",20623,], ["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,], @@ -1289,35 +1329,39 @@ 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",20567,], -["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20567,], -["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20567,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20567,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20567,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20567,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20567,], -["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20567,], -["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20567,], -["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20567,], -["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20567,], -["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20567,], -["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20567,], -["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20567,], -["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20567,], +["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20623,], +["features.home.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.home.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20623,], +["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20623,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20623,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20623,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20623,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20623,], +["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20623,], +["features.location.impl.share_ShareLocationView_Day_0_en","features.location.impl.share_ShareLocationView_Night_0_en",20623,], +["features.location.impl.share_ShareLocationView_Day_1_en","features.location.impl.share_ShareLocationView_Night_1_en",20623,], +["features.location.impl.share_ShareLocationView_Day_2_en","features.location.impl.share_ShareLocationView_Night_2_en",20623,], +["features.location.impl.share_ShareLocationView_Day_3_en","features.location.impl.share_ShareLocationView_Night_3_en",20623,], +["features.location.impl.share_ShareLocationView_Day_4_en","features.location.impl.share_ShareLocationView_Night_4_en",20623,], +["features.location.impl.share_ShareLocationView_Day_5_en","features.location.impl.share_ShareLocationView_Night_5_en",20623,], +["features.location.impl.share_ShareLocationView_Day_6_en","features.location.impl.share_ShareLocationView_Night_6_en",20623,], +["features.location.impl.share_ShareLocationView_Day_7_en","features.location.impl.share_ShareLocationView_Night_7_en",20623,], +["features.location.impl.share_ShareLocationView_Day_8_en","features.location.impl.share_ShareLocationView_Night_8_en",20623,], +["features.location.impl.share_ShareLocationView_Day_9_en","features.location.impl.share_ShareLocationView_Night_9_en",20623,], ["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",20567,], -["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20567,], -["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20567,], -["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20567,], -["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20567,], -["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20567,], -["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20567,], -["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20570,], -["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20570,], -["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20567,], -["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20567,], +["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20623,], +["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20623,], +["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20623,], +["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20623,], +["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20623,], +["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20623,], +["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20623,], +["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20623,], +["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20623,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_0_en",20623,], +["features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en","features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en",20623,], +["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20623,], ["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,], @@ -1327,107 +1371,106 @@ 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",20567,], +["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20623,], ["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,], ["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",20567,], -["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20567,], -["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20567,], -["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",20570,], -["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20567,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_0_en","features.home.impl.spacefilters_SpaceFiltersView_Night_0_en",20623,], +["features.home.impl.spacefilters_SpaceFiltersView_Day_1_en","features.home.impl.spacefilters_SpaceFiltersView_Night_1_en",20623,], +["libraries.matrix.ui.components_SpaceHeaderRootView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderRootView_Night_0_en",20623,], +["libraries.matrix.ui.components_SpaceHeaderView_Day_0_en","libraries.matrix.ui.components_SpaceHeaderView_Night_0_en",20623,], +["libraries.matrix.ui.components_SpaceInfoRow_Day_0_en","libraries.matrix.ui.components_SpaceInfoRow_Night_0_en",20623,], ["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",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20567,], -["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20567,], -["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20567,], -["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20567,], -["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20567,], -["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20567,], -["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20567,], -["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20567,], -["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20567,], -["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20567,], -["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20567,], -["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20567,], -["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20567,], -["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20567,], -["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20567,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_0_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_0_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_1_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_1_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_3_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_3_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_4_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_4_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_5_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_5_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_6_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_6_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_7_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_7_en",20623,], +["libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en","libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en",20623,], +["features.space.impl.settings_SpaceSettingsView_Day_0_en","features.space.impl.settings_SpaceSettingsView_Night_0_en",20623,], +["features.space.impl.settings_SpaceSettingsView_Day_1_en","features.space.impl.settings_SpaceSettingsView_Night_1_en",20623,], +["features.space.impl.settings_SpaceSettingsView_Day_2_en","features.space.impl.settings_SpaceSettingsView_Night_2_en",20623,], +["features.space.impl.settings_SpaceSettingsView_Day_3_en","features.space.impl.settings_SpaceSettingsView_Night_3_en",20623,], +["features.space.impl.root_SpaceView_Day_0_en","features.space.impl.root_SpaceView_Night_0_en",20623,], +["features.space.impl.root_SpaceView_Day_1_en","features.space.impl.root_SpaceView_Night_1_en",20623,], +["features.space.impl.root_SpaceView_Day_2_en","features.space.impl.root_SpaceView_Night_2_en",20623,], +["features.space.impl.root_SpaceView_Day_3_en","features.space.impl.root_SpaceView_Night_3_en",20623,], +["features.space.impl.root_SpaceView_Day_4_en","features.space.impl.root_SpaceView_Night_4_en",20623,], +["features.space.impl.root_SpaceView_Day_5_en","features.space.impl.root_SpaceView_Night_5_en",20623,], +["features.space.impl.root_SpaceView_Day_6_en","features.space.impl.root_SpaceView_Night_6_en",20623,], +["features.space.impl.root_SpaceView_Day_7_en","features.space.impl.root_SpaceView_Night_7_en",20623,], +["features.space.impl.root_SpaceView_Day_8_en","features.space.impl.root_SpaceView_Night_8_en",20623,], ["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",20567,], -["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20567,], -["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20567,], -["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20567,], -["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20567,], -["features.startchat.impl.root_StartChatView_Day_5_en","features.startchat.impl.root_StartChatView_Night_5_en",20567,], -["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20567,], +["features.startchat.impl.root_StartChatView_Day_0_en","features.startchat.impl.root_StartChatView_Night_0_en",20623,], +["features.startchat.impl.root_StartChatView_Day_1_en","features.startchat.impl.root_StartChatView_Night_1_en",20623,], +["features.startchat.impl.root_StartChatView_Day_2_en","features.startchat.impl.root_StartChatView_Night_2_en",20623,], +["features.startchat.impl.root_StartChatView_Day_3_en","features.startchat.impl.root_StartChatView_Night_3_en",20623,], +["features.startchat.impl.root_StartChatView_Day_4_en","features.startchat.impl.root_StartChatView_Night_4_en",20623,], +["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",20623,], ["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",20567,], +["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20623,], ["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",20567,], +["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20623,], ["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",20567,], -["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20567,], -["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20567,], -["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20567,], -["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20567,], -["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20567,], -["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20567,], -["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20567,], -["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20567,], -["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20567,], -["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20567,], -["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20567,], -["libraries.textcomposer_TextComposerScaledDensityWithReply_en","",20570,], -["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20567,], -["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20567,], -["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20567,], +["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20623,], +["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20623,], +["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20623,], +["libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en",20623,], +["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20623,], +["libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en",20623,], +["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20623,], +["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20623,], +["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20623,], +["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_0_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_10_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_10_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_11_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_11_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_1_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_1_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_2_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_2_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_3_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_3_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_4_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_4_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_6_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_6_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_7_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_7_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_8_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_8_en",20623,], +["libraries.textcomposer_TextComposerReplyNotEncrypted_Day_9_en","libraries.textcomposer_TextComposerReplyNotEncrypted_Night_9_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20623,], +["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20623,], +["libraries.textcomposer_TextComposerScaledDensityWithReply_en","",20623,], +["libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en",20623,], +["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20623,], +["libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en","libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en",20623,], ["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",20567,], -["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20567,], +["libraries.designsystem.components.dialogs_TextFieldDialogWithError_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialogWithError_Night_0_en",20623,], +["libraries.designsystem.components.dialogs_TextFieldDialog_Day_0_en","libraries.designsystem.components.dialogs_TextFieldDialog_Night_0_en",20623,], ["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,], @@ -1440,17 +1483,17 @@ export const screenshots = [ ["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.threads.list_ThreadListItemRow_Day_0_en","features.messages.impl.threads.list_ThreadListItemRow_Night_0_en",0,], -["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20567,], -["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20567,], +["features.messages.impl.timeline.components_ThreadSummaryView_Day_0_en","features.messages.impl.timeline.components_ThreadSummaryView_Night_0_en",20623,], +["features.messages.impl.topbars_ThreadTopBar_Day_0_en","features.messages.impl.topbars_ThreadTopBar_Night_0_en",20623,], ["features.messages.impl.threads.list_ThreadsListView_Day_0_en","features.messages.impl.threads.list_ThreadsListView_Night_0_en",0,], -["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20567,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20567,], -["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20567,], +["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20623,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20623,], +["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20623,], ["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",20567,], -["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20567,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20623,], +["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20623,], ["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,], @@ -1460,18 +1503,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",20567,], +["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20623,], ["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",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20567,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en",20623,], ["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,], @@ -1479,64 +1522,64 @@ 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",20567,], -["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20567,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20623,], ["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",20567,], -["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20567,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20567,], +["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_7_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20623,], ["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",20567,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20567,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",20623,], ["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,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en",0,], -["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en",0,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en",20623,], ["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",20567,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20623,], ["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",20567,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",20623,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20623,], ["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",20567,], +["features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithThreadSummary_Night_0_en",20623,], ["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","",20567,], +["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20623,], ["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",20567,], -["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20567,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20623,], +["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20623,], ["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",20567,], +["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20623,], ["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",20570,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",20570,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en",20570,], -["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en",20570,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20567,], -["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20567,], -["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20567,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_4_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20623,], +["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20623,], ["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",20567,], -["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20567,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20623,], +["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20623,], ["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",20567,], +["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20623,], ["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,], @@ -1545,8 +1588,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",20567,], -["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20567,], +["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20623,], +["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20623,], ["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,], @@ -1561,8 +1604,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",20567,], -["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20567,], +["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20623,], +["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20623,], ["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,], @@ -1585,84 +1628,84 @@ 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",20567,], -["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20567,], -["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20570,], -["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20567,], +["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20623,], +["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20623,], ["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",20567,], +["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20623,], ["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",20567,], +["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20623,], ["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",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20567,], -["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20567,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20623,], +["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20623,], ["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",20567,], -["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20567,], -["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20567,], -["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20567,], -["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20567,], -["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20567,], +["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20623,], +["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20623,], +["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20623,], +["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20623,], +["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20623,], +["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20623,], ["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","",20567,], +["libraries.matrix.ui.components_UnresolvedUserRow_en","",20623,], ["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",20567,], -["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20567,], -["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20567,], -["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20567,], +["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20623,], +["features.startchat.impl.components_UserListView_Day_0_en","features.startchat.impl.components_UserListView_Night_0_en",20623,], +["features.startchat.impl.components_UserListView_Day_1_en","features.startchat.impl.components_UserListView_Night_1_en",20623,], +["features.startchat.impl.components_UserListView_Day_2_en","features.startchat.impl.components_UserListView_Night_2_en",20623,], ["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",20567,], +["features.startchat.impl.components_UserListView_Day_7_en","features.startchat.impl.components_UserListView_Night_7_en",20623,], ["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",20567,], +["features.startchat.impl.components_UserListView_Day_9_en","features.startchat.impl.components_UserListView_Night_9_en",20623,], ["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.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20567,], -["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20567,], -["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20567,], -["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20567,], -["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20567,], -["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20567,], -["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20567,], -["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20567,], -["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20567,], -["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20567,], -["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20567,], -["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20567,], -["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20567,], +["features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Day_0_en","features.userprofile.shared_UserProfileHeaderSectionWithVerificationViolation_Night_0_en",20623,], +["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20623,], +["features.userprofile.shared_UserProfileMainActionsSection_Day_0_en","features.userprofile.shared_UserProfileMainActionsSection_Night_0_en",20623,], +["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20623,], +["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20623,], +["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20623,], +["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20623,], +["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20623,], +["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20623,], +["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20623,], +["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20623,], +["features.userprofile.shared_UserProfileView_Day_8_en","features.userprofile.shared_UserProfileView_Night_8_en",20623,], +["features.userprofile.shared_UserProfileView_Day_9_en","features.userprofile.shared_UserProfileView_Night_9_en",20623,], ["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.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20567,], -["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20567,], +["features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Day_0_en","features.messages.impl.attachments.preview_VideoQualitySelectorDialog_Night_0_en",20623,], +["features.preferences.impl.advanced_VideoQualitySelectorDialog_Day_0_en","features.preferences.impl.advanced_VideoQualitySelectorDialog_Night_0_en",20623,], ["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",20567,], +["features.viewfolder.impl.file_ViewFileView_Day_3_en","features.viewfolder.impl.file_ViewFileView_Night_3_en",20623,], ["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/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/watchers/DefaultAnalyticsColdStartWatcher.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/watchers/DefaultAnalyticsColdStartWatcher.kt index e596bc35a5..8659fc9634 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/watchers/DefaultAnalyticsColdStartWatcher.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/watchers/DefaultAnalyticsColdStartWatcher.kt @@ -56,8 +56,9 @@ class DefaultAnalyticsColdStartWatcher( override fun onRoomListVisible() { if (isColdStart.getAndSet(false)) { - Timber.d("Room list is visible, finishing cold start check") - analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStart) + analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStart) { + Timber.d("Room list is visible, finishing cold start check after ${it.duration}") + } } } } diff --git a/services/analyticsproviders/sentry/build.gradle.kts b/services/analyticsproviders/sentry/build.gradle.kts index 02dde35ef4..3350df864b 100644 --- a/services/analyticsproviders/sentry/build.gradle.kts +++ b/services/analyticsproviders/sentry/build.gradle.kts @@ -50,6 +50,8 @@ setupDependencyInjection() dependencies { implementation(libs.sentry) + implementation(libs.coroutines.core) + implementation(libs.androidx.annotationjvm) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) diff --git a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt index 0843c97a78..4e4f08b632 100644 --- a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt +++ b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt @@ -33,7 +33,6 @@ import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.android.core.SentryAndroid import io.sentry.protocol.SentryTransaction -import kotlinx.coroutines.runBlocking import timber.log.Timber @ContributesIntoSet(AppScope::class) @@ -132,7 +131,7 @@ class SentryAnalyticsProvider( if (sessionId != null) { // This runs in a separate thread, so although using `runBlocking` is not great, at least it shouldn't freeze the app // Also, the method is fairly quick, so the blocking shouldn't take longer than a few ms - val databaseSizes = runBlocking { getDatabaseSizesUseCase(sessionId) }.getOrNull() + val databaseSizes = getDatabaseSizesUseCase(sessionId).getOrNull() databaseSizes?.stateStore?.let { transaction.setExtra(AnalyticsUserData.STATE_STORE_SIZE, it.into(ByteUnit.MB)) } databaseSizes?.eventCacheStore?.let { transaction.setExtra(AnalyticsUserData.EVENT_CACHE_SIZE, it.into(ByteUnit.MB)) } diff --git a/services/analyticsproviders/sentry/src/test/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProviderTest.kt b/services/analyticsproviders/sentry/src/test/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProviderTest.kt index a8dc9f3086..ae1ac82dda 100644 --- a/services/analyticsproviders/sentry/src/test/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProviderTest.kt +++ b/services/analyticsproviders/sentry/src/test/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProviderTest.kt @@ -9,7 +9,6 @@ package io.element.android.services.analyticsproviders.sentry -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.itf.VectorAnalyticsEvent @@ -27,15 +26,14 @@ import io.element.android.services.analyticsproviders.api.AnalyticsUserData import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.tests.testutils.robolectric.RobolectricTest import io.sentry.Sentry import io.sentry.SentryTracer import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import org.junit.Test -import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class SentryAnalyticsProviderTest { +class SentryAnalyticsProviderTest : RobolectricTest() { @Test fun `init enables Sentry if DSN is present`() { createSentryAnalyticsProvider().run { diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt index d6effec06f..33c2ee133b 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt @@ -34,6 +34,8 @@ interface AppForegroundStateService { */ val isSyncingNotificationEvent: StateFlow + val isSharingLiveLocation: StateFlow + /** * Start observing the foreground state. */ @@ -53,4 +55,6 @@ interface AppForegroundStateService { * Update the active state for the syncing notification event flow. */ fun updateIsSyncingNotificationEvent(isSyncingNotificationEvent: Boolean) + + fun updateIsSharingLiveLocation(isSharingLiveLocation: Boolean) } diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt index c9fa31caca..9dd37af732 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt @@ -20,6 +20,8 @@ class DefaultAppForegroundStateService : AppForegroundStateService { override val isSyncingNotificationEvent = MutableStateFlow(false) override val hasRingingCall = MutableStateFlow(false) + override val isSharingLiveLocation = MutableStateFlow(false) + private val appLifecycle: Lifecycle by lazy { ProcessLifecycleOwner.get().lifecycle } override fun startObservingForeground() { @@ -38,6 +40,10 @@ class DefaultAppForegroundStateService : AppForegroundStateService { this.isSyncingNotificationEvent.value = isSyncingNotificationEvent } + override fun updateIsSharingLiveLocation(isSharingLiveLocation: Boolean) { + this.isSharingLiveLocation.value = isSharingLiveLocation + } + private val lifecycleObserver = LifecycleEventObserver { _, _ -> isInForeground.value = getCurrentState() } private fun getCurrentState(): Boolean = appLifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppForegroundStateService.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppForegroundStateService.kt index a61733cc22..9174b92684 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppForegroundStateService.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppForegroundStateService.kt @@ -16,12 +16,15 @@ class FakeAppForegroundStateService( initialIsInCallValue: Boolean = false, initialIsSyncingNotificationEventValue: Boolean = false, initialHasRingingCall: Boolean = false, + initialIsSharingLiveLocation: Boolean = false, ) : AppForegroundStateService { override val isInForeground = MutableStateFlow(initialForegroundValue) override val isInCall = MutableStateFlow(initialIsInCallValue) override val isSyncingNotificationEvent = MutableStateFlow(initialIsSyncingNotificationEventValue) override val hasRingingCall = MutableStateFlow(initialHasRingingCall) + override val isSharingLiveLocation = MutableStateFlow(initialIsSharingLiveLocation) + override fun startObservingForeground() { // No-op } @@ -41,4 +44,8 @@ class FakeAppForegroundStateService( override fun updateHasRingingCall(hasRingingCall: Boolean) { this.hasRingingCall.value = hasRingingCall } + + override fun updateIsSharingLiveLocation(isSharingLiveLocation: Boolean) { + this.isSharingLiveLocation.value = isSharingLiveLocation + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8c7b608465..db03a32f18 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,8 @@ dependencyResolutionManagement { url = uri("https://www.jitpack.io") content { includeModule("com.github.matrix-org", "matrix-analytics-events") + // Required transitively by androidx.media3:media3-exoplayer-midi for MIDI playback. + includeModule("com.github.philburk", "jsyn") } } google() diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index 4acb06305f..848777464c 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -20,6 +20,7 @@ android { dependencies { implementation(libs.test.junit) implementation(libs.test.truth) + implementation(libs.test.robolectric) implementation(libs.coroutines.test) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt index 12cfe44b44..d7ce9e2d28 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt @@ -6,15 +6,17 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import io.element.android.libraries.designsystem.utils.LocalUiTestMode import org.junit.Assert.assertFalse -import org.junit.rules.TestRule import kotlin.coroutines.CoroutineContext object RobolectricDispatcherCleaner { @@ -52,7 +54,7 @@ object RobolectricDispatcherCleaner { } } -fun AndroidComposeTestRule.setSafeContent( +fun AndroidComposeUiTest.setSafeContent( clearAndroidUiDispatcher: Boolean = false, content: @Composable () -> Unit, ) { diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt index d78f570a31..232116b385 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt @@ -6,10 +6,14 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalTestApi::class) + package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.annotation.StringRes +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.assertIsDisplayed @@ -27,17 +31,25 @@ import org.junit.rules.TestRule val trueMatcher = SemanticsMatcher("true matcher") { true } -fun AndroidComposeTestRule.clickOn( +fun AndroidComposeUiTest.clickOn( @StringRes res: Int, inDialog: Boolean = false, ) { - val text = activity.getString(res) + val text = activity!!.getString(res) onNode( hasText(text) and hasClickAction() and if (inDialog) hasAnyAncestor(isDialog()) else trueMatcher ) .performClick() } +/** + * Press the back button in the app bar. + */ +fun AndroidComposeUiTest.pressBack() { + val text = activity!!.getString(CommonStrings.action_back) + onNode(hasContentDescription(text)).performClick() +} + /** * Press the back button in the app bar. */ @@ -46,6 +58,13 @@ fun AndroidComposeTestRule.pressBack() { onNode(hasContentDescription(text)).performClick() } +/** + * Press the back key. + */ +fun AndroidComposeUiTest.pressBackKey() { + activity!!.onBackPressedDispatcher.onBackPressed() +} + /** * Press the back key. */ @@ -57,12 +76,12 @@ fun SemanticsNodeInteractionsProvider.pressTag(tag: String) { onNode(hasTestTag(tag)).performClick() } -fun AndroidComposeTestRule.assertNoNodeWithText(@StringRes res: Int) { - val text = activity.getString(res) +fun AndroidComposeUiTest.assertNoNodeWithText(@StringRes res: Int) { + val text = activity!!.getString(res) onNodeWithText(text).assertDoesNotExist() } -fun AndroidComposeTestRule.assertNodeWithTextIsDisplayed(@StringRes res: Int) { - val text = activity.getString(res) +fun AndroidComposeUiTest.assertNodeWithTextIsDisplayed(@StringRes res: Int) { + val text = activity!!.getString(res) onNodeWithText(text).assertIsDisplayed() } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/robolectric/RobolectricTest.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/robolectric/RobolectricTest.kt new file mode 100644 index 0000000000..cf5f584f7e --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/robolectric/RobolectricTest.kt @@ -0,0 +1,22 @@ +/* + * 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.tests.testutils.robolectric + +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Base class for all Robolectric tests. + * + * It is now configured to run by default on Android API 36, + * waiting for Robolectric to support API 37. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [36]) +open class RobolectricTest diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/robolectric/RobolectricTestParameter.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/robolectric/RobolectricTestParameter.kt new file mode 100644 index 0000000000..e8866d5685 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/robolectric/RobolectricTestParameter.kt @@ -0,0 +1,22 @@ +/* + * 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.tests.testutils.robolectric + +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestParameterInjector +import org.robolectric.annotation.Config + +/** + * Base class for all Robolectric tests with parameter injector. + * + * It is now configured to run by default on Android API 36, + * waiting for Robolectric to support API 37. + */ +@RunWith(RobolectricTestParameterInjector::class) +@Config(sdk = [36]) +open class RobolectricTestParameter diff --git a/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt b/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt new file mode 100644 index 0000000000..9f91f30816 --- /dev/null +++ b/tests/uitests/src/test/kotlin/ui/LayoutLibErrorFilterStatement.kt @@ -0,0 +1,40 @@ +/* + * 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 ui + +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Workaround for Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 bug where + * HandlerThread_Delegate calls Thread.setPosixNicenessInternal which doesn't exist on JVM. + * Tracked in https://github.com/cashapp/paparazzi/issues/2342 — fixed in layoutlib 16.2.3. + * Remove this workaround once Paparazzi ships with layoutlib >= 16.2.3. + * + * Paparazzi's PaparazziLogger collects background thread errors and re-throws them via + * assertNoErrors(). This Statement wrapper catches the known NoSuchMethodError so the test + * can pass despite the LayoutLib bug. + */ +class LayoutLibErrorFilterStatement : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + try { + base.evaluate() + } catch (e: NoSuchMethodError) { + if (e.message?.contains("setPosixNicenessInternal") != true) throw e + } catch (npe: NullPointerException) { + // Also catch this error on `HorizontalFloatingToolbarPreview` and `HorizontalFloatingToolbarNoFabPreview` + if (npe.message?.contains("""Cannot invoke "java.util.ArrayList.size()" because "childrenList" is null""") != true) throw npe + } + } + } + } +} diff --git a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt index 6f8fe2f506..f420110f31 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewA11yTest.kt @@ -29,7 +29,10 @@ class PreviewA11yTest( @TestParameter(valuesProvider = ComposableA11yPreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule + @get:Rule(order = 0) + val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() + + @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor( preview = preview, locale = "en", diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt index efb1a82cba..17632c212d 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard1Test.kt @@ -28,7 +28,10 @@ class PreviewShard1Test( @TestParameter(valuesProvider = Shard1ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule + @get:Rule(order = 0) + val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() + + @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt index c3312bad3b..cec2b028ac 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard2Test.kt @@ -28,7 +28,10 @@ class PreviewShard2Test( @TestParameter(valuesProvider = Shard2ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule + @get:Rule(order = 0) + val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() + + @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt index d4508fb821..c752d0b321 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard3Test.kt @@ -28,7 +28,10 @@ class PreviewShard3Test( @TestParameter(valuesProvider = Shard3ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule + @get:Rule(order = 0) + val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() + + @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt b/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt index c0e3b807b7..55881de079 100644 --- a/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt +++ b/tests/uitests/src/test/kotlin/ui/PreviewShard4Test.kt @@ -28,7 +28,10 @@ class PreviewShard4Test( @TestParameter(valuesProvider = Shard4ComposablePreviewProvider::class) val preview: ComposablePreview, ) { - @get:Rule + @get:Rule(order = 0) + val layoutLibErrorFilterStatement = LayoutLibErrorFilterStatement() + + @get:Rule(order = 1) val paparazziRule = PaparazziPreviewRule.createFor(preview, locale = "en") @Test diff --git a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_0_en.png index 8ab5f8a44a..559c023aa7 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d225730fee6fae0d93396a6c47dcd7abd86ba8902ac521c96e05fa36f619bbd -size 25840 +oid sha256:ba67fa9a541371f84cdc16dcc0c069e0cfda486c2b21dbb885cee7e0d9b6b005 +size 26924 diff --git a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_1_en.png index e2037ca626..788daaf8df 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27f94a461f5b4bc5289ac41691e3736a73ddcbb9e1f2a1d0c9390a0ce5cb44b -size 27919 +oid sha256:00f8ec369fa64c6bbd97b37a5b53481438e74abdb5d366d32e3acae29460659c +size 29075 diff --git a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_0_en.png index 9df8fe1585..6ac6642b35 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:708a157d8a7cb5da2b2ea52c12f9b10c172ec380dfcec09583adfe2ea4af3d74 -size 24610 +oid sha256:0a38ade8b9a5e3ce85fbe63ae76d2e856ac46c6cdc012a4952cb191fcbe8979f +size 25577 diff --git a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_1_en.png index 00e1659da8..5451e763d5 100644 --- a/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/appnav.root_RootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:260ea7d9f891afdea78a6d546138cbad7586fe2210058d3774d549c9918c2ccd -size 26529 +oid sha256:1ae9d78458ea0b9d4b258be0620698abb699bcbc70275d98adfd992d8d50ec59 +size 27541 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_3_en.png index b5e7419267..b45afb67bf 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48fa7b1415694f0a7ebb3de458edee8792f5643f681cc25627560f2e3d8ba491 -size 16331 +oid sha256:49731638f35e9c7583ece7122e3690728f3d4183a65e2e49cd270d02fb93ac70 +size 15950 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_3_en.png index 1c8ce2991d..14fd900451 100644 --- a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebf559265b2cd4ebba7c5439b759365ed8d31532988bda5d8c76bb7c8d76dd68 -size 14892 +oid sha256:11a77776662896532ef2999e3b90aa93b3459bf9b7a5f461c06ccb3743612aab +size 14738 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png index 13a1a88a4c..ceb4413a16 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf9aa6f47084124e7c774ae78bc98404d960805bb1338784f3641251e33ac7c6 -size 29426 +oid sha256:a79354416a9a99eeb652b0a9381e694529b10e930d82bb04ae175223fe4ee4a2 +size 28736 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png index 2e3058ac63..c762b76dd2 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d40447ece02c06d429d52138550c73e559f89dfd99f6dff7a92cfc3103f2dbe -size 34093 +oid sha256:145e1ac30630e4f13df29fc92b2e4f305ec014fe85a4c178e6bc0bb76041587d +size 33942 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png index a581a42dfd..6aee832eb6 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:654d8e0acd3b3bbfb856414b5e5ca5615f3febacd8bebaa9af5a1e4d96c5d035 -size 41543 +oid sha256:f9adf6cd9c1d5a0afaa94228037646835c4c22d4ab664d44405f387c6546bec3 +size 41019 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png index 33c4f82363..7f77ca2730 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea98589cd86e69ae6d57566b205b4dab70f5f4b5c8b0fee09c7e690cafa7f914 -size 42514 +oid sha256:47121746126e9d867c80a6426d851100ab81db82e9585d3e5219a2b677d26ce3 +size 42009 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png index 7600487323..0f5c6846f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37ffab3c3805f0bdafaca9035af6e4f706286f45a05a761ba7e7c160d45463c1 -size 44226 +oid sha256:02fefe256f1a0f9c5c0ad464eeddd70e8335a11cc6e73bbc5d04ff3a3ff0ea0f +size 43728 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png index a581a42dfd..6aee832eb6 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:654d8e0acd3b3bbfb856414b5e5ca5615f3febacd8bebaa9af5a1e4d96c5d035 -size 41543 +oid sha256:f9adf6cd9c1d5a0afaa94228037646835c4c22d4ab664d44405f387c6546bec3 +size 41019 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en.png index 05a54538a4..6f7fced7b2 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c20f293ba37e9af3e9f045de2ff1746af5e265d5dcf5bce5dc396da5c29387a6 -size 42482 +oid sha256:1b9f48a2166442bb5a3555128af31d79acf82746517b1352021289a852864061 +size 41981 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en.png index 5ff5af29b1..144166d170 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fea7e63fd1d31db6500c4b110b9c05190da7b7201e1029177f8b5a61e728e100 -size 37458 +oid sha256:d7b55ab3959b1189d0af772f47da0751844e8ea59fa65705baba0d6828fc3c03 +size 36858 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en.png index e51dd98b4f..577de0f744 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edc093b514b7e648d176a17b0c368fbed82e9c46d3f584fdb9c6094a6e5aba55 -size 40420 +oid sha256:f4c3e7f20ce61d54845022afef017edcd1d8df5f15116ad3ba04a6529a5900c7 +size 39765 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png index 4dd2b02ce3..63547859f9 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e4dfa9c766681c750383863450aff0052f3083d495fcea1be74446ceed1cc4b -size 30462 +oid sha256:5de69f76240ae56cb28d4819f9e787f280c8a2cc4cc61aab2505f144414c6488 +size 29810 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png index 79155ebbfd..85071dc677 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49309d3b55bf7a4313ab100d54f2d13b741b5bace7fec7d03e8e30765d8e100b -size 35329 +oid sha256:631fe0c5bfdecb8782a46005edfd92e1b42fd323a316b2084a48118e0ad49c35 +size 35260 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png index 5baf7f75b8..a1dbe81d8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea38acc7ef6fb8307b83453eae80ec7c3154a80d0591c4f1fc1a87aa581a7926 -size 43124 +oid sha256:55c66afbe31070cb85746b2ff1897517b4839a9d5767caf1050265f3c10d195c +size 42701 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png index 920269d44d..765f6713e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fd71e26763fb648a7b6ac898f2b354056354d1deee3d3712786a0c36e0cf476 -size 44168 +oid sha256:508a81fa57693cfd414db0b16e81fe9441b0fb06b43b4ef9a4b4b685a7ab1b08 +size 43752 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png index 65bd6527c1..463bf3963a 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d355371ce78c9a78900a6d76899289844c687b794c2afe127580a7c50b25935 -size 45949 +oid sha256:bd5d3ee7b7b5c5f08c9461fa79e98dd08ac791f29f3ef3637b9a8ddd54f036bc +size 45561 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png index 5baf7f75b8..a1dbe81d8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea38acc7ef6fb8307b83453eae80ec7c3154a80d0591c4f1fc1a87aa581a7926 -size 43124 +oid sha256:55c66afbe31070cb85746b2ff1897517b4839a9d5767caf1050265f3c10d195c +size 42701 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en.png index 875f4a71a3..e652f66e33 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c73b89404caa0fda5233c7dfed5810276a5cb1c1efd5843484b80c7f859b99a -size 44209 +oid sha256:d7fb6d97314e612541c29bde743190a8b6ecafb22abfb9257b4b3351010acbb7 +size 43774 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en.png index 27b3f41f26..4854bb4608 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:005c6520e884641ca1ca4e1248627937806328fc4ae527b811c65ee933a5616e -size 38889 +oid sha256:bc921fdd55417c80aaf9ed33b9b30b623120c6aec0aa0f19810f4ed91435bba0 +size 38304 diff --git a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en.png b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en.png index c29181ca85..560ddb5530 100644 --- a/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.createroom.impl.configureroom_ConfigureRoomViewLight_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fac6f85109de1b6beb4cff51a91dec8e34ed8ed50820829b63584226e611d5d -size 42064 +oid sha256:cee85a4b9f76fef13813cc0a8542d5c0e767e9be529c5c877c30158225767c45 +size 41406 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png index 934cf82e03..c6103fdd16 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:501543b209eb2f4a28d0368ae49ef9703a6dff8af67194412b3efe51ea140146 -size 41455 +oid sha256:1da296f6409e018bf6e36d4bf8d0183327e2da1730008721476397a3398a2e13 +size 41382 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_2_en.png index adab865ffd..cc54eeeef0 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:152c46640d6bef83fb6c2950a9f54e81314fbe912133cd852f53392ff87dde2f -size 13749 +oid sha256:b3fdd076498f0a2cc41104b6098005a9418e7eb16863e6c9040e0a49d6dc3922 +size 13739 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png index 273750675e..1f93f01826 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:422607aface1a2f436431ea431111f5a003cf75e4eac4d2e27687b45e10c0c05 -size 59476 +oid sha256:7456163ece98c73691a5147e3c7b183189a545389699bb9fe33a8ce68bb125e3 +size 59685 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png index d05a334223..73c8da0c71 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8f87cd003427ebc919ccf0a5679849eddb210231257d1c78af202ff761425cd -size 41145 +oid sha256:bbfb4004f0c9b017a86096d9a45c1793ef32d5117f6a17c59a41920264e95399 +size 41099 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_2_en.png index fa444dd818..bbca53db2a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac359458a55030fda5714e1e513fd5e45a837df48f6234c6d61fcd3c783e4b76 -size 13310 +oid sha256:7afac8b6173f573544b987363e1ecf5fe2251ac23182128bcf2c27e290253dc2 +size 13319 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png index a80c52a517..f293dd7cfd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_RoomListContentView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b56cf113f934b6e882a21e25596cfba606fdbc7cab1853bf37d365f192d47908 -size 58581 +oid sha256:683568f509c83c24f506924e7a715f6ff9530d3f6ae28e041436288a510785cf +size 58742 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png index 6b4037b3eb..83ee2f4342 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c755a7dcfc2c9f48d449e570a3f1af1cde299fd90ddd4478e9a4c315cf03128 -size 23810 +oid sha256:aec4d2f2ec1b277afa895e47739b503d86b3104d315b6d20fce223324c57f51b +size 23960 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png index e64ae4c3cf..84b0af9394 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efa5408c3af0fcd39659b8d30b21ace43be46d2876815ad592f802a7887b5eb9 -size 23678 +oid sha256:82f48053958a8bd72245365ee0cc4c1649c5c06dde6f66c95c23bf5009edd2f3 +size 23879 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png index 82b04fc045..d7fec262ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fc512e71e168473e0e976be3e3e6cd24b455273257ad0adf970fc2641da43bb -size 25679 +oid sha256:e50eaee3dc1512f76566d1170c2db8a07230d7c9f159c5f52c43ac9e810e2a72 +size 25938 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png index f52d785548..8a06c99645 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de71af50bba66a285b5ef44e1a4fe76ce6dc2e094c4c09ddf701deb9a5db898d -size 22025 +oid sha256:3abd359bba25e19e231d723cc3de7dcf394c35a5948a935b2703d81940e00120 +size 22391 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png index eef2177c3b..9fa6aa895a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88b3583d55c6855027f13a152a6fc414f6b9e11d65e5b5403463b84fafe38c3f -size 21891 +oid sha256:4910c5904a53dacc578b32d0033e3fe64fa084602b62dff13930b68ee83e0cf5 +size 22298 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png index 87d3f56720..f7c703f8ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.roomlist_RoomListContextMenu_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e64367832735139b68269a8c679b9c6f2b929b906124a0ee93735cbdd9202d6d -size 23826 +oid sha256:82ed1ed8042d4d6f9633144cc5f34567cfc09c6303e005a7157954917837054d +size 24160 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png index 526bd69d4e..c69cba2125 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35c1709b912da1102a50aaa4f8470cbf05a75b03669aec51b90354ccb2b09ba9 -size 85336 +oid sha256:09c0613d19abd7673223bc5eac12b4e87c73e66e7ff9d437c5f97ae8bcaf9303 +size 85239 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png index de86c4285d..bd9fa30de2 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d69d687090971ab7e9b87f959521109fbed48413ef217e1be3da8d7d985038b -size 38751 +oid sha256:df0dc04b3946a58dda5c3499d965ab23cd3c172eedcb8a23461201c2aca95934 +size 38713 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png index 0b5629c139..8ec9b1585e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f60372df2ed22a4c52624e16324f7da1f6000eafdd519f84d42a29acec016ee -size 83600 +oid sha256:e28f120a919db7c7a0ced5ca0d56239197785a0ecfbd37af0c5dacb5f8316c1f +size 83583 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png index 07e9ef1e28..72e173c493 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spaces_HomeSpacesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a6a7f3188eb9635ade7c0bbc2239257cfbe3166f4a6fb7917a1e5578f51d9e -size 37436 +oid sha256:cc37ee6efade5947d0fdc25f6925cf45ba55442eb3fb715f942178513a00e8fc +size 37416 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index 1bec9cf123..344bd350d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f2357a181fcea22deb153e4d4c570421354cbc74f3e272a6e94c7795cf29c4c -size 144183 +oid sha256:b5dde8ed5c776f4701f24dd7d411dd78f48adab261efad71e961c845e24d12f3 +size 144367 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png index 3a262e8d47..b5f80f2faf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ce82255e49c97179a216124ccfcbc6726a8053acef767041ba44304e7f4735 -size 67756 +oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 +size 67694 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png index 6eed7b2e94..f2a69ea475 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2339985aba8128dd562491d6575b92338be2338c090d978f553a1e50b44b24f6 -size 32073 +oid sha256:6b493bc5151f15d6ba91bfb0c27b41f3abcf13ee7d511aa59db171cecdbd95f3 +size 31958 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png index 4feecf5107..f4dff45422 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8c5c2fed11674afda7ec3a838d749a4f688a2979f934970d2b2d3ff7a837f89 -size 94736 +oid sha256:1a33681fe064d5e12f53273bfd9262af8c7b902e237cb8d0641573b0645e291c +size 94687 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png index da72b71928..bdad242551 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b54ba16e7e2413dd994bedcc31e5ffdf6c291487ccc5ae17178d2572a4fb0ea -size 87282 +oid sha256:022954bdca89572b74bc70f6d91820e99eef8826409c0a06e6145a5c3ec8eaf4 +size 87268 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png index 3a262e8d47..b5f80f2faf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ce82255e49c97179a216124ccfcbc6726a8053acef767041ba44304e7f4735 -size 67756 +oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 +size 67694 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png index 3a262e8d47..b5f80f2faf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ce82255e49c97179a216124ccfcbc6726a8053acef767041ba44304e7f4735 -size 67756 +oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 +size 67694 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png index 5c05d6e633..e01335907a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69fd2195c4d0203b602ba91b822c5e9dc0e902b77dcf1352daabaf91aaea1768 -size 65301 +oid sha256:25e5ac35ddf79fdc78c21dbd031f506da04caac7bdd53f279c88fd4574f72a82 +size 64644 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png index c33ecadd3d..fe41a54fcc 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e64f044d19a709056de64976c6902baba7821885f35cd88873d5462fd52c3e4c -size 57343 +oid sha256:8c1245da0a4cf56aba8e102e7f297996ed58fc8c50165288dea11ca86fbc0dcc +size 57235 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index 3a262e8d47..b5f80f2faf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96ce82255e49c97179a216124ccfcbc6726a8053acef767041ba44304e7f4735 -size 67756 +oid sha256:8247dedc9811f2b325568973a2b714cd4b46322bcd0bf89af0e662518fa77747 +size 67694 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png index 99940a4489..91f02401b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e74b9b1819bf778ddb397a40113f71b2eb0ada0dcb26af85dab26bbc53ed079 -size 54210 +oid sha256:2f19f7f9fd7675dd46fd61764e54ffa02cfacd08173aee0d86512bcdc5f30fb2 +size 54466 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png index 2532044b27..f72667b724 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4e56509b4f10e97283d25432edd7be4b511987f0879acc3026ac16dab25b75c -size 54012 +oid sha256:31217a40625020e07d4f6d6d98d1d5db9d9fabbf0a9f3e4461e72c26e5447925 +size 54272 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png index 0baf9cf023..f64c1b8a19 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe76cc652842cce967d975fae7a387f5cff118153b345bda452d5dbc618b22f8 -size 52255 +oid sha256:4e3c83b95b9b5fbcc07e5d84cbc6d3b54d08e1c5964bfc461be1a9c9c33e6e18 +size 52539 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png index 36b337448b..b6779aca93 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcd14b5720ecc9a80f6f2cc7133d6c8695096b9c90d3504380464c46c91048c8 -size 87131 +oid sha256:0769e243dc60de52c16df23a86eb62167bb60cb756d113f88b9b476c025e402b +size 87117 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png index 63ff6e0ae9..fe171e26b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8518ca0e40ddff8b8f8cd1960a5cc8c4419a7f7381459f2bca782df80cb0094 -size 62676 +oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 +size 62572 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png index ad828b778a..f36b5c2f83 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14d762574470814f5b37cc05a2167d546f92460bd016206e71ba43026d00f9cc -size 26743 +oid sha256:6c3fb8ae530989e5a6d6e4193fbac4d4356af34076b684b782a0f8fec31226f1 +size 26657 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png index 5af2cae9e7..48b07fc9bd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41252714e511c1c63b43edd35c2f443a7a791a2b2f29885efc1f2384764edac3 -size 88578 +oid sha256:5d302be6dbaef08b501f4f33d3e01338e068767702a7018bd5bb24ade6eee2cb +size 88558 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png index 280257f53d..5cb7d766cd 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e841a58c50c98444bcc48316654ee5c664cf845e3c38f4bd02e80ce8d3c9366 -size 81261 +oid sha256:6be5cd35eec917bf1c96d03079df8235d0bc7b52159f54c8a177a64f648e58c2 +size 81212 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png index 63ff6e0ae9..fe171e26b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8518ca0e40ddff8b8f8cd1960a5cc8c4419a7f7381459f2bca782df80cb0094 -size 62676 +oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 +size 62572 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png index 63ff6e0ae9..fe171e26b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8518ca0e40ddff8b8f8cd1960a5cc8c4419a7f7381459f2bca782df80cb0094 -size 62676 +oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 +size 62572 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png index a709ce6a75..89c4934dd9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c10e44c82a7412fd51a651a69b9c643a7b1eafcc4251227009e44a47e9649bb5 -size 60656 +oid sha256:8172ca867a8c2e827ead36a22cefe967a5b57c69cae6d5aca1c941e4dd2ba16f +size 60160 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png index c2dc739e38..390c9f6818 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d923c6e3503af723269dbe5c03f1024e7e3fb6ba746fb979f7735c0ad3b8e32 -size 52322 +oid sha256:13740b4fc4cbd24075606e45bf070a8fbb762d298857bc071399def91dda7175 +size 52189 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index 63ff6e0ae9..fe171e26b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8518ca0e40ddff8b8f8cd1960a5cc8c4419a7f7381459f2bca782df80cb0094 -size 62676 +oid sha256:8a7b2a6ccb43dc572140e2ce658967cd273d1122bf0216269a648920099a68d0 +size 62572 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png index ead9ed0d6d..66f1bc4364 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2d17c9021392f93fc022ce0ba4305f0739acaeab3ebfd51b7d3294ace365807 -size 51156 +oid sha256:9bd841b458babd88d364e4906d199e48491407547430269dfb7b95a5d3055de6 +size 51527 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png index 33f2b46a34..1264ea44d8 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:286af36457c34621e10b91226b13c5a9fde77af6b63827d39a0a95697972d4ab -size 50896 +oid sha256:5828d3c51711976aaf8906eee401866baa6683e879d5f29da27a2d2110ab1a14 +size 51291 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png index 733a66bb38..89af663213 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34a5fe14ee5ede42d962e657e4338f58f56e79e0f9a3f65ba24e37810a9c1250 -size 49205 +oid sha256:4fdf0d6bbb3a760fdfc93e13c4acbe5703d1fe1b99c1e859897a8dcd319eca3b +size 49559 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png index a408245bae..1092dcd1b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b09c1c639b12b0521da9e4b403530d4bd33f6e79823903091870fba56477153 -size 81113 +oid sha256:bce0bc8590037705369735e83e576f0818f4528298b4b2face999c8802f855b9 +size 81081 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png index bc93c4c316..a0ab0a2192 100644 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95096ef9f55d57ea4774f8eaa91d92a01fd44eafd4670442d3db132ff843ede0 -size 21476 +oid sha256:62fee8e8966ff4df0d951aff1d13e8578c9c5a55e89c4407dcc8077c9b1b72af +size 22249 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png index d1c7598599..cce585fe3f 100644 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b53f36378be4b4da2608210cb3134923c6f4bf565652c80a1b45cad8216d67da -size 25485 +oid sha256:d3226560d55a9fde8175206c0ce21cb4cd90d900d357003496bf74b719281166 +size 26191 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png index 54a3c425c1..6a278db2b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d549ca3000080a6d0c416d7a45d530da96c73184309048126be6304393bc74f9 -size 20082 +oid sha256:643f712799406118deb87af3fab34e8838878dc04e0935b4e54dafaaebd03479 +size 20895 diff --git a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png index 341bcca1a6..2b09d75bc9 100644 --- a/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invite.impl.acceptdecline_AcceptDeclineInviteView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a61181eab30d30f56c5407bb42395a268ff80f7dd455ea04a468f92739e0a87f -size 24194 +oid sha256:c2638b57f0f4ab1876429f096065d121122eaca0b9b06600281526dbf51d7791 +size 24843 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_0_en.png index b658085113..31e1e4049e 100644 --- a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:948478d8dd340d0cb5facf43af64d726587be6ebf07314b854cb562aab7a47e5 -size 37273 +oid sha256:673519b0fde86c46908d614cf116041c1bd27a81d5a87955febc33187398559b +size 37304 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 index 7281594c56..9a4dbeeee5 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c81c5e7e1a62ed5fc55d672de612c5ace83561f70a3e363ecc851a0ae3658b81 -size 54766 +oid sha256:4ec919634968a61656ff3668937cc49a6de659e265ca0e4a1a3c03533bbd9981 +size 54751 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_1_en.png index 52949fb4a2..ae37022419 100644 --- a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c81346eada72651d9400de736e56fc94330ffd014ab762a9abb1890bfab1f75 -size 50285 +oid sha256:6e77a040286b900514440d410ca2bf0ce826e0e1364ccae6e1c34d7f0204631d +size 50615 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png index 52949fb4a2..ae37022419 100644 --- a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c81346eada72651d9400de736e56fc94330ffd014ab762a9abb1890bfab1f75 -size 50285 +oid sha256:6e77a040286b900514440d410ca2bf0ce826e0e1364ccae6e1c34d7f0204631d +size 50615 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_0_en.png index 6910d23bd0..e102a3934d 100644 --- a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:965946244a5a38f44fd34d4383448b3b92ce5481d2f992f7f72b69ded951c375 -size 37617 +oid sha256:06ab4c46d877199855b0901ec96adb74fe9ca3fc484c71a0fd472d388a7dafd0 +size 37518 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 index c66df232c2..df970a313c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d143c9fb43b79e3a6248b4eb179228b91652f43013da73d10fc5ec09384c20a9 -size 52486 +oid sha256:6855898359711e2d2bc80403e063ac8c51ad7e09bec9fae6ad5d765d9e3cd129 +size 52497 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_1_en.png index 5092d9cb3e..0a8f04940b 100644 --- a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:601916fd979efa36104cb252a76a523e25a86cdfdca1cbd4e6f35314ab977d26 -size 51358 +oid sha256:7aa0ca8af987ba61d12a52f8298ab344ef7f5464a1f33c55607e559919da5a31 +size 51764 diff --git a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png index 5092d9cb3e..0a8f04940b 100644 --- a/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.invitepeople.impl_InvitePeopleView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:601916fd979efa36104cb252a76a523e25a86cdfdca1cbd4e6f35314ab977d26 -size 51358 +oid sha256:7aa0ca8af987ba61d12a52f8298ab344ef7f5464a1f33c55607e559919da5a31 +size 51764 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png index 33110584a9..f0bfd0f2b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbdf9740221b82aaaf332c95e7299594f0b4fdd258b2e9c733133bdd4a01f692 -size 28378 +oid sha256:e82c6e4ff563e7ae870431528d9413ac915e16ca0151aa3c86ff5081bfc72478 +size 27563 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png index d7e2d0a3f9..9a2a2a4a0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ccea3d95f047fffdada68e6ed02c359a552d5c2c2f6be488c96f759bc208f37 -size 52830 +oid sha256:1cb6be576055a020b5c5c6a996e5923490d6a36ad5ea8246cd6fca9707affda1 +size 52987 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png index 42ecbe8a68..438d96608d 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:471abc7c54d0f90ad5a923bd21abbf0960bbb9cbbdb22313a779083baf61ab3f -size 40671 +oid sha256:ae0b92a52a7f2c0533af7d7f0fbddc6497778e7c5dcedc3344a2ba60a6a948f8 +size 40439 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png index e07bbc74b5..eead87f8f4 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f588a2d2299825159380a42df4acff302a8b50b7dbc3cb02e145b596ead20061 -size 51525 +oid sha256:7041e20c1d2b83b6ef199fda02f594c4406fb6061b1706d6530d36b3007bc582 +size 51574 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en.png new file mode 100644 index 0000000000..18b186ce98 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23d54e777c8c9ade84bec155e3aef42f5b1c13b98a23ea6ecbd956d0090b5ad3 +size 32186 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en.png new file mode 100644 index 0000000000..d1698e913f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.confirmation_CodeConfirmationView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bde248827b1c53b66c5ea7112af24cac43aea378266e3d7b7ad8a5fa2047258 +size 31261 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_3_en.png index 3832b8d087..ee619462ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe92b35b0fe2e5481eeb497f2ada1a639cb1a66f937b4245798edffea75b1f5c -size 35932 +oid sha256:17c58ce74a1b198f8659636d180a98152bd66be212f381afd652f615bd847a7f +size 28795 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_4_en.png index 4821d3ab76..292e8f0ead 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f60e02e48d483fe8d6b2bdf46192ccc8911fad1e79c7ac1e0272824cc04d3d1c -size 35648 +oid sha256:90e602edde377f866071ec78f391bb7cd0d9b59bec79be91d6a42108f604a32d +size 35833 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_6_en.png index 720026f561..6c9c87f774 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60333d74c8d940b32621defc5cdb3c3776b2980c137244dd34304365ee9ff447 -size 24158 +oid sha256:ced04df939214952c513aa8b4ef4c7a02dfe3bb3bce81f9ec6ea1827f639bf3f +size 24471 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_7_en.png index 0fcd59d198..429daba97a 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bec4c7d06dc2cc287d2165f588c6764e5314287119960947d7e24dbc19ce901 -size 23971 +oid sha256:018e6226239c439fc683eead0acb733d1e06eebad54b1a6c2b43d8cbd2e822cc +size 24324 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_8_en.png new file mode 100644 index 0000000000..14a6d77fe9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94e0cec5dac39083da711bdd30762eb84c794ecc41ab74f6dbe8cf5053402ea1 +size 22080 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_3_en.png index 368e908499..21fe725606 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f6bc4bb2233067ab6d07e596f8d5ca294501a47a21451a1115aa46e238b31a5 -size 34996 +oid sha256:5493215960a23d305a2f9f26e39b307c735167583c20ac3fafb1b67e89aa1d12 +size 28124 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_4_en.png index c9d622683c..51d6a1bc13 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:766e65cfceb2e9ff3895ecd5c2be2b80d7b2833b30a5c67a0075ef823bb77cc0 -size 34816 +oid sha256:31b97291b16fd0872891ad2341d95d34d8c0608f92e7a5e39b5f27d40769b111 +size 34955 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_6_en.png index ab0b09c575..3bd50c5655 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd50a4cb9848602b08b853f4fa36314b981a858b98349d68decfcbd6f402d719 -size 23631 +oid sha256:43cf679fbb9a83f14209e61129afb8e7f18a60f9bc127bfcf4f5dff05c79598e +size 24033 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_7_en.png index 4194458de9..2a85b2cf7f 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e2de9b362092dbd08deadac70591d4682304c69981fe27cbbeae4f26233d4aa -size 23444 +oid sha256:7ee6f9fbf05261efe52c247da3869c68c6e87f5132c52da72e56cf833ea6864d +size 23833 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_8_en.png new file mode 100644 index 0000000000..d36887d3e5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.error_ErrorView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:144ea7ed8bba7d5ded34b5e00d7f3f036c4dc882be0cf8a644817ffee4c531f7 +size 21429 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en.png index 136812ccab..474a2b43a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11a877e3e4810777b7ddb15fa11b8410976142ee4fb5e9d4103f4248acf121cf -size 29685 +oid sha256:cc0409c3e6c60994c81d83c429bbfb7ebc4ee53ac4114d1913b089a8068ca51e +size 30046 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en.png index e04f62d6aa..fab5d228a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:471129545a85e702e8b59391f47630f128f80e819b042e91ee356684e4ae2438 -size 29680 +oid sha256:3670e0f777491b7f6101c3c1c3cc006e4848a8f6e481b9740fb42ee8807ccf9e +size 30024 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en.png index e5f8926345..764cb92eef 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a932c055ae0704cd62358c5442658c1cb059d7d3c0c56518de3e763c8eab3c52 -size 30355 +oid sha256:79b7dd684dcec4a06271eed34794e7546ba441ad2cc9796641ba570871e45cd3 +size 30659 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en.png index a1d475ecdd..9a795c080c 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ca49fb335880670a3b71fdd1f2e2be50e9a5a6677d2e17e9534c9724d9d55b1 -size 30534 +oid sha256:29233a6bad671608f5e1a1d20b24b454cfd70f8a846e4d9572cc86b8c7da45a0 +size 30843 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en.png index 116337e4be..58ae956b13 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be4f85040190c658d5734000e602b4cb43cb0a5ab2e2cea0a993959702c4722f -size 33681 +oid sha256:5d30ae8d9d3b3820dc17d7aff3bd5c6794f5a267993a5dae4dc601278ccaaa5c +size 33999 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en.png index bf1561d23e..7ba887d858 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29d7aac2eaef7850cc9b0f5583d6c9a8aef230525d40011a9a7cb5401b681d03 -size 33409 +oid sha256:bf5b6e85e57aef0d52253dc7563bbe0aae3f38e4a65e2fdc8e0460bccf26b006 +size 33722 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en.png index 99c888caba..a0404e5e72 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cb49a80d6ff771484743be2efc4900d053ccf84372614b98e9284d66d19056f -size 28895 +oid sha256:e0c446ba7fc8e0c0c68732ba34058b41e04d8641dcdbc83255f9417706044c8c +size 29067 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en.png index 4415136e95..fedf612386 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68845451924a562238f2c75dced10697cae75171d976e19c0de927eacfdc3210 -size 28853 +oid sha256:da2778e384c9f749e888690660f496b4af93b7cff6f93c9fef346580a477e1cf +size 29029 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en.png index 13df6a6877..6faf2d1cba 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cc21d3fe55a5dba7b9e70d445877b07f0c88bee61fbd64f929a2321473ea8ad -size 29581 +oid sha256:62c79d08701dad73ab3f5f7744cd2683febeb34cb8bab986685e0cd8bd33c0f6 +size 29773 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en.png index 5e08ee2145..d4c10cf76f 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf6312a9e19892c9db949474cb9aa3b7572cc132d3d5474278abb3c4d91b7ab1 -size 29505 +oid sha256:fad2e4dbc822c79fbd8569dd8db7a5c08b335793aff6923129175bd4479cb218 +size 29744 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en.png index e84f38285e..ff882fdaea 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f24e410e2715019c5fbba40a2f79750781e8db0f4b36c772cb7b37f0d3b0d26f -size 32697 +oid sha256:7a85f79b0167efdc0277d05a41712c39d10115482242bb84a140c6647c28d85d +size 32895 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en.png index 16f85bba9f..1fbbba121f 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.number_EnterNumberView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd1e04c468c0daae10917f8e66a8e5a6ef8f72fa02f3ad4ca507b714dae7898d -size 32449 +oid sha256:865d4c7ff07834027411a76767aaaa8aa0febf529b910eaa51e5de9bd8a776fa +size 32641 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png index d9b4151b77..5bc0a7e57a 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f91ec4d46970dd80c97efc76361ce26b3e705b037b99bc032205ace3d2c8749 -size 31953 +oid sha256:da71297e7073c5c592dc154af7f137c4eb500bf528e1509b95da869a2c61cbd2 +size 30488 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png new file mode 100644 index 0000000000..d2ab2dcb1e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48cafd6b98791b64e4cc6a16c602f17e3a886659698c3c93bc4acaf875337e0f +size 31102 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png new file mode 100644 index 0000000000..c4593656d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.qrcode_ShowQrCodeView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f6f8d1282ae47a5240aec2b2c63a137b8e3e25ec8b746cf2d82d0fa7e0c0a34 +size 30287 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en.png index d6faf7dd8f..fb88b5b2a8 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b4a99e2ea65816e35ec8fb1cf40d8aafebb6fd5ae688911d7d009dda15cd0f9 -size 18123 +oid sha256:0ec014c00a19fb280f4e2047d873f1698eedde51a765948bd2652079921588a7 +size 18264 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en.png index db46722b7b..9bf1f8609c 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfbe2c73c732d671278ed411d76915d04d6e7ffce130c2a6dbe30ea43628e231 -size 24247 +oid sha256:ba8f1717d752a93cdda51a6ee5e8bac043a4572aadc4d1c0dba2bfe80e6c55db +size 24372 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en.png index c0703e447a..139ae28bda 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9168cc32d47f15de0ce961d841d872181b8aadf5151d4b5fd4d305c87cdda75 -size 21597 +oid sha256:7bc87f28234d11c58f5b43a7f87ee4e5a5d631e5a41ba11756c057a2a5eea1ff +size 21745 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en.png index 457e334ac2..a36301f230 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96448542138570c8615e24dd3b26be55145570c64ac9d410bbc4dac36280c345 -size 23769 +oid sha256:c97be1636f039afaab65c1b6858c439fcf47e6643681a886fd9c24ab6c7b0901 +size 23901 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en.png index 4621b6c95e..78e7e0d1bf 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:672e617ab62c72127a4c57bed6da8ba4710cfc24ca455ea6edc765d7a424a0f3 -size 33766 +oid sha256:90ca0d9059538259cd19e7e75f6adc11f21b3a1cc77aec8d5efaf3d55abf8505 +size 34006 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en.png index 52e64e87c3..e6153a4a2d 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:451565c6881885b51de299c2d71e77f78a62e68f96a75bc859050e64b87ef79e -size 17372 +oid sha256:1cd2061366439af2139a1ace15557ecdd8aa91c0d2f1e0adfc76a7d63e17133c +size 17615 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en.png index 5610b70d00..03c7fd3b0c 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:def1de8d3a76255e72e0d65dbbf6364f77c2764e2ac99b32a0145174cf0f3a28 -size 23244 +oid sha256:541b4cd380fee2f2a445fe25a1171492c865b10980dd79de70f42b0f3dc120c4 +size 23475 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en.png index 3784bbcac7..3cc9326747 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bd9567f7a504590da66993a2da6771c1a4157bc37301510f6d91d62efed7c16 -size 20745 +oid sha256:b71e2ba137ec1ffc65c07fc836074f7fa0ef310fb81a15db905db819ea29dd0e +size 20980 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en.png index e511df48c7..d6bb6bcc8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d662774653bcec6246ac6529ec98bcff77b02e4a6a8ef42ed0a640fed3770dff -size 22698 +oid sha256:de97697d988d0cc25626c7e31f153a7dca6987215edbe2e42ec16367ea9b4b3c +size 22952 diff --git a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en.png index d35cdd7397..59789476b1 100644 --- a/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.linknewdevice.impl.screens.root_LinkNewDeviceRootView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee6a0cf330b30e4fdc7cb8b4268251850f2f112e1ad0ce4af1166341f04a0638 -size 32103 +oid sha256:9b3a92c98a93b1d9e7f127a13c664c7275e26030d59a0ebcd3641428d9a21683 +size 32303 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Day_0_en.png new file mode 100644 index 0000000000..c431ee02e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaf113646fa3a8ffd57528d3e97eec05a8d80b99a3bbd7770266353dbe6abd64 +size 10217 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Night_0_en.png new file mode 100644 index 0000000000..e3a3776919 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.api_LiveLocationSharingBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53d9deec2a6295fe0155ce2986463a3b598436c0515ef87e0905c62c25970420 +size 9647 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api_RenderingMapsNotSupportedDialog_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_RenderingMapsNotSupportedDialog_Day_0_en.png new file mode 100644 index 0000000000..cf35ad0fa8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.api_RenderingMapsNotSupportedDialog_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f64455657f0f8a5ac7eec71ee206721a79b25dacf470d105a0e6497e570204a +size 23954 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api_RenderingMapsNotSupportedDialog_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_RenderingMapsNotSupportedDialog_Night_0_en.png new file mode 100644 index 0000000000..41cdb80dc8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.api_RenderingMapsNotSupportedDialog_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43bd5f04f170372b120fa2223c632d6a969c45b6d4fe61b161a7109318f9941f +size 22658 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Day_0_en.png index cedf0b1b7c..0473be6edd 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56a86695d2c25c94a8e79c27c8f525229cc348201073566909f83a681b37ac30 -size 251329 +oid sha256:69ffc87be8fc643d3cd553eb86140a9654ccac92d205b93e428d8631f2a770a3 +size 408053 diff --git a/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Night_0_en.png index dea0d3f21c..7b248fd2a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.api_StaticMapView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c2b2a7071bd1c21ff706525e2416169507fef364485c651c429baefa15d4c9f -size 104240 +oid sha256:7b2af9381e91c470a87d5585fd211b1f1ffdc4d56876285f73765a5ccd5ce72c +size 147867 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Day_0_en.png index cae8f75b66..afd74655d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaafea9efc1000495ee469797239b82193844caa3d6f98c0c3a4344a536a1798 -size 17155 +oid sha256:c9ebf3725fa875994cf1a15b30e2d4d533c2a9281f0f7d5d9f83f8685ba384d1 +size 18637 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Night_0_en.png index 29f70fc9b1..30fad74bc6 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.common.ui_LocationShareRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f113f8979679c0673e4cc1f691140bc570b6826bea23eecc403f2fbfd3f6d09 -size 16460 +oid sha256:65c321199578618012d27afe478c0eaf6a67c44101e7d1d1c51a4c6c1fa9b93a +size 17897 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_1_en.png index 8957a012f2..ba69ac150d 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d950639a9cc9961210b449e5c975b6f6bc890a47f4977eeaaa19b837458f66e -size 36475 +oid sha256:17ad972930d2b563e278c5eb1296ce12781b85e939d6769131de08fbab9f3f7b +size 38760 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png index bf25a07679..a7ce4c15ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a52d39cb27572c49ab64b1534ead353687d83870b5d5cfe7158859925b4717f3 -size 34926 +oid sha256:ffd4f5b8fece04b00ad5d866f6fc11e753e800f956c966e703d1dfc66f1ce8e7 +size 37257 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 74d6ac15ca..d70cd4ccd3 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:2621fef4175ad0f0982270284ffbbdb6b3b0534b09013c1cc378504a85d13068 -size 22385 +oid sha256:6ef91ce622a66945ef5475adbe04164635ca155a2a9e1682bc03459e9a6e289b +size 22356 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png index cce7a48382..db11b33396 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a97492422d54a6d6666c1ade693dc9b63bc9ca07c17d6c1f787c081984c09f68 -size 42470 +oid sha256:a1ffc8d536a239ca11de23f3f8e5fb14b429b9beee03c80bbca0321ec853f5e0 +size 26638 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_7_en.png new file mode 100644 index 0000000000..e8ae396119 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddaf978cdf3e70b01fbee75e7e7290fa390e749b48ae192fe7da0dcaa9a1d4dc +size 38417 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_8_en.png new file mode 100644 index 0000000000..cce7a48382 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a97492422d54a6d6666c1ade693dc9b63bc9ca07c17d6c1f787c081984c09f68 +size 42470 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_9_en.png new file mode 100644 index 0000000000..41a52e602c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9f4d7292e5bfb5bdcc699aa3a5d9e065ce66f232a3175294850539b70a2be6b +size 21674 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png index dbd4836467..aa539ed8a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3260d344a065f1710aff1450dbdaaa0bd1fbd963d5304167124d845c46be433 -size 19903 +oid sha256:dc1e9df75619bde2571bad0362f147c627ee44a780fbaa9a2c0e5a49e252d14b +size 19881 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_3_en.png index 2c8c53dc1e..08d4da7504 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8901e7adb15ff01e9c696729f1abc52c5fc8666bca030f449f8caf5a22615f53 -size 29196 +oid sha256:931f79cf0cabfe533162ad5b633c7261d06339073f3bd6c7044761aecc27ea42 +size 31143 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png index dbd4836467..aa539ed8a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3260d344a065f1710aff1450dbdaaa0bd1fbd963d5304167124d845c46be433 -size 19903 +oid sha256:dc1e9df75619bde2571bad0362f147c627ee44a780fbaa9a2c0e5a49e252d14b +size 19881 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_6_en.png index 541b2a97e1..06ec0c10e9 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:038e12f3caeef6ac8d5389b7cdc68138e089dcac335d0a5904adc55c9bcb7b1c -size 40642 +oid sha256:4031ae26bb8465a61021c15b6166a2186f8f99d7a4dbad7bb28aaf83493cbe69 +size 25907 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_7_en.png new file mode 100644 index 0000000000..7ce1496b52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:580ee0fe2b1dd97496850efd8d8f852046473f5acf3cb42dcf862ac3f6de2f59 +size 40266 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png new file mode 100644 index 0000000000..1f8c2d110d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f139526f69881e08b17d15013d78fb3eeca76eea5b18d378953c37fbd9d76921 +size 44064 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_9_en.png new file mode 100644 index 0000000000..213a75dce9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.share_ShareLocationView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5198742a388a2bd04985fe6b9f035de3878125607fe3b554209b0eb5c4590db3 +size 19935 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png index 46226555db..ceb1513af6 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdf7b194a075902ab9434e272865293535ef39370fbb9cb172b3cf8774850c73 -size 19104 +oid sha256:8988c700db517eef71d7f42c8e21ac819f51c92fb88c0c25cb400be7a5326c22 +size 19228 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png index ceb1513af6..e01e90f7d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8988c700db517eef71d7f42c8e21ac819f51c92fb88c0c25cb400be7a5326c22 -size 19228 +oid sha256:dea2fa898a6f9b921eca0931d7380814b31ba858b88c90ba1fbab6332b4c8ebb +size 20722 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png index eed60f472d..d3ee3b9e22 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da601c01dd487f9c66f78ada91954398e1dcdf699f1ba4f6d8f7661f7b8cc4b7 -size 18715 +oid sha256:ca86fd5eea8c05a52fee801e1ca61c2e4e205ad9874e06d69b1d1f674585f87b +size 18842 diff --git a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png index d3ee3b9e22..547e7ba364 100644 --- a/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.location.impl.show_ShowLocationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca86fd5eea8c05a52fee801e1ca61c2e4e205ad9874e06d69b1d1f674585f87b -size 18842 +oid sha256:f9f64bc6bb5956459f8943c05bbf0aa6ee9e97e3103713f34d49c013f86376a9 +size 18878 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en.png index 515976516f..3206f5061d 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ee51d6f3454f52bc4afe5d90245b853fb621b097afb1a2588a91e4d9dd69133 -size 16590 +oid sha256:c5d291053aab6e5781e4b252d11a36e596d4dd22a08a2b82e381bcf395f86e42 +size 16622 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en.png index 5081175acc..295525d526 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:164b662bc38dc07e7995879b2ed0c9ef685498d1082e27d8d28d372809a6afe2 -size 18886 +oid sha256:916649bc7621d7580c579b3520195eab03f32c174f3cfa5f32f95a19b234c316 +size 18941 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png index 5eb1b68db4..851adf4f5f 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e872428460462ac33f00c6cd903ce7345ac9afd9a3d02b37d763d129f7ff304c -size 28572 +oid sha256:fcb4dea012f06580b17e6406e862ff40e42486c30d00213f8161333dc2e57a32 +size 28565 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en.png index 5c9f60137a..34c22546a3 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:919a87e26608070d11faf06fca878643fd4090fb708f8b8e2127b8f49b6d4efa -size 15970 +oid sha256:cee47e791377ebd2eebda92de0127e5c04b64d84113f459abd731fe49b672804 +size 16000 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en.png index 24649e54f0..52081c426c 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:641d43a9325286e3d7facb4fe0e53a13fc89c3394c702699b2b5ed7db534bbee -size 18237 +oid sha256:f06158293bb8ce12435eb7b78c5d8aef3f92e6c847ab389de82e931dd6d3bfba +size 18254 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png index ab6d3a7fcd..b5e2a2576c 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c981543a80612ff3460bf4460b552218f8453a9b8981c57e41d9c89fe7e550f4 -size 27138 +oid sha256:27879114748c757e4e666151b953747c1cc0369e7d5ac1eb3f81e459e4928b70 +size 27168 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewCompact_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewCompact_0_en.png new file mode 100644 index 0000000000..10076e1c68 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewCompact_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7790ba46051ba39863f057bc53c937f513f91b6094094b2b449794d7de3aa875 +size 32833 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewCompact_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewCompact_1_en.png new file mode 100644 index 0000000000..eeddc7261f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewCompact_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc7032e0d69a44d8ab2d3187183eb77cc28d7a5669fd7e56ee99d4f90ee05d62 +size 33137 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_10_en.png new file mode 100644 index 0000000000..0dd66f5feb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25ca16b3574c5cf850e06099312d4af294561c8c9833742a0b3902bc3b35cb39 +size 20669 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en.png new file mode 100644 index 0000000000..5669a59d24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8665179304ccd2e0acf517ddcf369c4daae51c41732ee1b9618e3bedf38aeffc +size 31643 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en.png new file mode 100644 index 0000000000..265044a0b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14300f3af0bc8c003ce94868665547ad439826b47cf416f1a062ffa4a1d7e793 +size 15782 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_10_en.png new file mode 100644 index 0000000000..e03c2f9cc3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e58621f378a1abac78c5bdfb6a31675c1f762f53434f28b869ac8c7b6f9ad8ad +size 20109 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en.png new file mode 100644 index 0000000000..ff95140047 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88354d222679723cadfb76d2c4928fad78e17fec13514c0e4712e6f877b1a0c7 +size 29627 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en.png new file mode 100644 index 0000000000..4bc9f6653b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d505c4b2323dfc67c1b7e119960ede5b97b52d6c453610cdbb01efd485730aa5 +size 15369 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png index dd05b7e17f..88034ab8ba 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:011c3caceeb03e9162c5e60dd71d53212971e13b7a6b58692feb901ac13b79e1 -size 35844 +oid sha256:131578839b180f129a15d080c5580f255f52536dc3a6b595bee27574e8be0e66 +size 35847 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_10_en.png new file mode 100644 index 0000000000..adb9484e70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a60781d3bc38ed6c2ed830464f447d509f88a9767b81dea3acbd29ea21b5c0f6 +size 36212 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png index 680f363dca..8a66c7b727 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aef835a050377afd05e6a517107586160a86fce4f32344fd92d00a08b4a2c660 -size 36250 +oid sha256:3d6fdf4c5e49a9f29d10f8500cd0b0ac080cfed3e3776510ffd8ec8413be2a40 +size 36251 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png index 8e99b487cf..73267ca9c3 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f36922d058b19410a38456a9e29760f9b890c1fc0549b3aba2a0e76d3dc64c54 -size 37079 +oid sha256:0b3753087c932d0cc83277959546c7d288592853479eb3f718bb552bd4f87995 +size 37073 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png index 6a8ae3dc0d..74eb3256e7 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a90bdec84019b24913e647f61a5223d9283a8e9604e8446032acce2a71b6da21 -size 40871 +oid sha256:01a4a3a8a4552e068adb3c45f71479120506ff5f84b0938d44174e03b271c9f5 +size 40867 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png index 0af4ff0fcf..aafb60d1a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec435ce434370cb5ca711458b955e8843872b01675234eab15cad4ab6e08dc38 -size 33542 +oid sha256:93d813e1f76cf2576be1063b4ea91474014fe57eefab1e277c277c19f44b0900 +size 33549 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png index 3bc7a1d72e..d2b52d0095 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:412549704d3c5007a46b3b3a115a7cb7f89a687f427436236d4b51efa09c9bad -size 38247 +oid sha256:17bca98e4f7c0a17a9a990ec968634eabd2b774c561a43fa6a0be0ad0697c8a5 +size 38240 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png index e8ad13d33f..0654018b0a 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3f4d74da073c904db3f7ddd9a5be9e49161c64b0210fbc2c09dd755d2fadacc -size 29238 +oid sha256:00757d73357d0f1cf217cdec35c514a519af008514f2d4d8e2c3b540a84d7950 +size 29218 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png index 9dd19fc627..346d6053b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98252e4f345d6b34e37a8b065cb570f85b5678f6d81b36ef96ca3c5df9433a23 -size 29804 +oid sha256:cb8ee0599cbd6774b6ce9fe48ef532e93f07b1e726799482a1c265ca5a6733a2 +size 29778 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_8_en.png new file mode 100644 index 0000000000..4a24e4d762 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e142a576df4b442c3e10a6347510ff8a8e243cada0bba35ee94ceaef6a349995 +size 37700 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_9_en.png new file mode 100644 index 0000000000..4d65f3c480 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Day_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d39b8e5898470ee8802bb15ae0433bcdde60b982e88ac823f5d5baaec40a641 +size 31436 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png index 9d1bd22ef5..08a43fe01b 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e87b9820e45b16a13d2f9fb0a470b5f57abf0e45f48d16e5049c4abca04e94ea -size 34629 +oid sha256:df7a30ab0bc53d84bd7c0848795c7b1e55140b1c40162f16fa54ac6495730de7 +size 34642 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_10_en.png new file mode 100644 index 0000000000..1831605a28 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d115c0f6d4c705dce1062aa2dbb026980df5efed8d8b2500774e6f75cbe366f +size 34987 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png index bf74b3022e..b8f5c8a44b 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74e88064bd3817c79443b6ed2df9b68e9e455d7bef2d94590854ce0d937e2107 -size 34997 +oid sha256:a2ec9d1e862444773b670c51f0ae18093fcb4a3900403ac041389b9d7b70eb36 +size 35010 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png index d5087096c0..4b25422e6b 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d3ded6d54724df9d03d382e12b58fc23970c354de52dc90cb0b13b66c9c614 -size 35729 +oid sha256:b7d2a247664c21d28a5554a3a720cf1de59a31ad58474b0d78e3e845b04e22be +size 35731 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png index 7a4a9fac87..7276bf4f44 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de86916ae6798afbc59dae5075a5f678010e6bff78a91cd06d40a7cc19187e83 -size 38344 +oid sha256:b732e81b6e9691182e441d1c83906f62dcfe93a41d871ff0f8b7a27bd9a0b817 +size 38366 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png index 22553fcf66..7ff75baf69 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94ed59c67dacb5066006189c0daf6d55704495ecffd145d69ea3ba968ffda246 -size 32467 +oid sha256:66e164942227a9209a928fbd0eba211818f0c53b25ca65886fe95ef1c0bc4cfc +size 32482 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png index d807d65d99..a19beaca81 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:daadf3522ac14a9ad2d0d5d497e6e688e69cc1e2ff824eaa5a0002adef1a980c -size 35888 +oid sha256:97bb8124c590e9e4f33203853abbb41aa672871e03c548209ada0ee39e97a824 +size 35913 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png index 913655b87d..8c62f0546b 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d090ac1696261be94b5073faf8e7864fc36ffa378bbf22e62e201cca4e7da3c1 -size 27869 +oid sha256:d8dfd1e4ab4eac0b6208bd33332e5b2537bae6dc74d6ba8f0654c1c995b65519 +size 27894 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png index 9de7f9b9b3..c90256c1d8 100644 --- a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d98c1f00e81a318d5df078cb91f88b1a16fe01bf37b835d9c8b976045d4b3023 -size 27860 +oid sha256:d7c66a024081eff6dfe43f72087651351290c66fe8cd6a3f1d98f31f7800ca8b +size 27894 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_8_en.png new file mode 100644 index 0000000000..8080cdb8a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_8_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71d30a04330bc8d5c6332c2f741ace766f6942d40a4641362088ca4819a79366 +size 35413 diff --git a/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_9_en.png new file mode 100644 index 0000000000..aee389854a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.lockscreen.impl.unlock_PinUnlockView_Night_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bd95ec6a8977e259b568d824b9ea70915d93b53ac81df333e799c8aa62edd37 +size 30322 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png index f35d541afc..c9bf9e3c1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b768e63862ed7f9c355a907b80bc144d012e66c012438ffdfc65652a8d6318bd -size 5992 +oid sha256:0990301d92d4862054ba5e54103dac359df37997242498766854d7bb13431657 +size 6068 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png index e82d91a78f..fb829c4873 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7945b3bcbe86c2adf888c89f23e0cfb76f5ac30d3f37408ad376af35f5b5fd3 -size 5920 +oid sha256:3022b2b813d9a3f77b0ba72706f0fb870a1c3f7b4a8efc90e611b66b6e9bdf8e +size 5926 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png index 747a677a5d..0b20380cd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8217f8ca87864ecc8184ac665adfa31c461b42ec4313c349363b824dc6138235 -size 17795 +oid sha256:49371b85e60a5e0465c0ba1537293f6c071962c498579bd71b42763671c8dc01 +size 18148 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png index bc7a9c237c..cbe30336d4 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6e6e4c1b666db69ae67b0ae4bc84756e490c02cf7c3e261c2897637f90d642e -size 7681 +oid sha256:78ef17d2f96d5544ecc8213fe1b7f78ddf263557855901f00f152a0b543c1a6f +size 7746 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png index f902686235..447de4de32 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce2b95b979aa2406bcca270b49131aa24b11b325333cf3827e93df03758a3d85 -size 6839 +oid sha256:ce777b1c6895caacc21adf4f6c10c71f75a2c6d31a617afe1c7f49d10c5141b3 +size 6929 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png index 7b111f303d..ae99061ada 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5cce43ca6b6dcfc1461164145279bb8e4bb1f3d2d2976a8ed22edb030fc50e0 -size 5713 +oid sha256:f49c89cde78d8d8971d5866f3cef2fef51bc36d1844b0364c950dc2afb188c5b +size 5707 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png index 345e7d769d..359e866105 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de7f8d4bcf900c38e3a8f16f317a9a16c352977f2dbe9213937f09f1990a54be -size 17004 +oid sha256:759f68aae091a43f54d597e386a5503692f99ed9c3908450dfadf59999047d01 +size 17272 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png index c0c462a052..3f58435f8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83bbd72e29e24256bfd907334f8603f56c306eaba520f03d21ca5bc6388047dc -size 7395 +oid sha256:0591f839e4605cbc51746f9d479a0bfc37e078178e6b2d1395db07dc8a7e1261 +size 7382 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png index 2963e20187..abcf602e31 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c72a00088338657055c29bfd64e8ea11b47d4875ebaf7ac8ba45b02b31d16109 -size 6617 +oid sha256:a57866d169c69ba68365ef8cfd4739ec95f8a8aecf4805ec68b2f247f3f698dd +size 6627 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png index 6856a2501c..fe5123f403 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af5e1cbfd118b6eef4ab55eff57e2f735d0d8280846e57e1106228dbff575e6b -size 5594 +oid sha256:db8eda0daf8e2038c2c69ff945e686a9e5ff271d8dc4f30987f14f7acb22ae85 +size 5625 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_5_en.png index 16668cae13..c4a683e701 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d62d11d8d8ccbbbbe811f157ecc20f31096a017f4acb36061a29e2aaefbd3e8 -size 25132 +oid sha256:44e078c64ab932a6e9ce2d429efb5f44556d4a923f4949e75dd2bd99591c4533 +size 25242 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_5_en.png index 20a22487c4..2a6cbdf049 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6d0f6746f6ccea9db09a6d351e5aae7d10d4d623edf00059bfece647731c2e5 -size 23959 +oid sha256:a2d413d264af067d4e03743cd50be879c91796a3f0907f40cefcdc6b0e466897 +size 24020 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png index 16668cae13..c4a683e701 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d62d11d8d8ccbbbbe811f157ecc20f31096a017f4acb36061a29e2aaefbd3e8 -size 25132 +oid sha256:44e078c64ab932a6e9ce2d429efb5f44556d4a923f4949e75dd2bd99591c4533 +size 25242 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png index 20a22487c4..2a6cbdf049 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.login_LoginModeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6d0f6746f6ccea9db09a6d351e5aae7d10d4d623edf00059bfece647731c2e5 -size 23959 +oid sha256:a2d413d264af067d4e03743cd50be879c91796a3f0907f40cefcdc6b0e466897 +size 24020 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png index 415cd8ef36..23858ce575 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8dd6bc040641b68274f838a02bb0bc7750d35814bd8717c332ce6c4b4bbab47 -size 47001 +oid sha256:c7544e2566517e8692a1cccd4607bc2ab0f2767bbe9e5b39eb439d7695ad2ce5 +size 46905 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png index fe703a5bb2..9b505f3cf9 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8213893828ba44ae9ea6777204e922c11e584cf428ff29fe98e809292a34b3c3 -size 44816 +oid sha256:b5dbf45467305e83f302fd626a2c0adcd27eac6aeea8b7a83efdb6e728d974da +size 44703 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png index cceff9bd6e..815986cbc3 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75f10457c010805b854c02d3cf7744e1b8c2081d6d33cc9315123ce51cc9c3cb -size 45934 +oid sha256:41907de78c86fca804af2af7dd351db6a1ab87515e71bb906ffa2219ad4f5879 +size 45880 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png index 21020974de..f08662715a 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f224eab6216782450b3075d6d63f8bda4764683d86ab683696bfae71d062e41 -size 43678 +oid sha256:62581b532695d10a42432dfcb37249c28c2bd617ecb49d7f3a8224855b78ae55 +size 43600 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png index 0bd68821e3..7869ed3dc5 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12ec8a85ff40410c93a3f5dc974b95d1d138712f2cc6f81fc18b9bb7a7e1af3b -size 22486 +oid sha256:8098b9ff6c4d08d8cc61bc51e391a5a17e9791d7236d2d7cf652a6b855c87b97 +size 22501 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png index 5801447ff5..c1f55034ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:929b3c6c51c3ca37e6d00dbf9e1f3024c009cd0123e845293fde23b1cfe4f9ba -size 23459 +oid sha256:94878580a4ffff7c4d437eba1c62184aa61435420d6c85488fefc53e33e1e150 +size 23497 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png index 96ca1664f8..a960bff528 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7dfafac3676b9cb89886b4c1c3b2384dd7f75652a1c013889dacf7d5ecb8773 -size 23974 +oid sha256:31cee1e647c74c67740320180af54509e466ad162ed56894695b038ccbe35103 +size 24017 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png index aad9addcfa..fc8aeb2409 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b8b39f7e59e23e9be33e5b25375403e622935e709bae3c7669c80a350d19d25 -size 21998 +oid sha256:8bdca1cc8a99976a8c4a297192650a496a4347dc27c73d1950f693be1cf2cabf +size 22005 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png index c78ea7e982..aa2b293c3b 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbd62f37c69fa71c52442494efa24ab2d9b56c96dd9ba0d9e35a04917dcfb8e1 -size 22895 +oid sha256:604243eb5409e170a760cda08fca1662b7694c6920f7b18da8d4c4f9d60ffe9a +size 22915 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png index d62536a9c0..1f6dce1201 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.chooseaccountprovider_ChooseAccountProviderView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:906f38a6ea86f1794e86ff4a4f20bf620f280114e234ab35db7165644b8665fc -size 23403 +oid sha256:5d3bb32677abc81ffcb892839c5609f27637a9ea8081460d3f95751a03f125aa +size 23426 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 index 47145ebd49..3314252e26 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62df5826abbcd47bc2a18743f2baeef8c9c85ebc547660a9ae92328cd49499b0 -size 62768 +oid sha256:6e97cab70b9ee3e870154ad4ae4cf1bf0bb413facf8253f5f9fe9429eddf76b1 +size 60303 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 index fa68b6c988..d58e079ced 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c92a6ada8c2aa5991c28935dafd8e35a0c0efc2c40660c16105b3cf381914cdd -size 61097 +oid sha256:caaa2a0f4de455704a6b5c78e19c2f723be7deaeec470c77ff1e87df194dd95c +size 58620 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en.png index 878e97a06e..048c9beb26 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbcf2216fe8f2653d44df345c157eb66f54ff8767c210bfeab5719d6d70230a8 -size 31686 +oid sha256:647163b31578572c9bf4a63a1d110f732fc3b0c41c3991651996608d6265c536 +size 31960 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en.png index 05edb58b0f..d27a10bf39 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:324b1392f40bb408b6469e656e774f5b1f7cbd9bb6557ec7a2587a971b548ccc -size 31288 +oid sha256:6e447151f2a7aaf9052130d5b915e9aea8180880c66fa51afb06d212079a0ced +size 31159 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en.png index 096b521d75..4b7e849116 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1075e98dac6a50a282a39dfd5855833080becd377e1bf3f9db8fb20104bda80d -size 33479 +oid sha256:e215ffdf8ba90e3f7a047d25206dfef4a08d25137ee2e4d3e5c16f254282927c +size 33371 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en.png index e738c998a3..276d9919f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f55c2e968f464ba9593dc8841a0e633c2f0a75cb3f85857aa3b4b716770629e -size 30831 +oid sha256:76613fd85c3211bd2c96e50f2d481d2d994aad20368713c1da309d0147ce1101 +size 31015 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en.png index ae7ffe2dd4..c1da84be8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8484c5300a8da5b120a24513b29bdd697dd3edd7d6b7c91e5925aa55c6290fd -size 30366 +oid sha256:5f32133324f20fc97c3473ca6ea3b1705d3b0beab5bed11af9199e4f5b9ab124 +size 30410 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en.png index 1cf5572c4e..e06e5ccaaa 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b87fe16a96a8c34b49aab481f65c3223fd098360048b7dcd1ee8bef85fdf378 -size 32650 +oid sha256:00556b6c310f63b548bc1a88b3b882a93412a2a7dacff008862faeb2ad572eb3 +size 32671 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en.png index 98039ad35b..d13b526c94 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:647b5d2512180674b14a93903de4ee0539720dc1ddc7eaadec23940aa8b3befa -size 35934 +oid sha256:c95c7e7f68133f25bca9c33246983cdd1b448ca822b1221eba54c171c8f2e051 +size 35898 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en.png index 9751c25cb4..aa8c5c7858 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:010a03fc52fb0ec9b39701d75ef26c7f23ba6195e7d3e0c4e2b7e03518425b57 -size 24287 +oid sha256:b4b6b193a6c9bd9188cde36056a512ebe10f9b24d07a33926b51876712b453b2 +size 24468 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en.png index 0fcd59d198..429daba97a 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bec4c7d06dc2cc287d2165f588c6764e5314287119960947d7e24dbc19ce901 -size 23971 +oid sha256:018e6226239c439fc683eead0acb733d1e06eebad54b1a6c2b43d8cbd2e822cc +size 24324 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en.png index ce263c7d02..0fc27b0af4 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb22d79ff43ce9a1931537880829cf5d2c13aafe3c06136c80703f2599d676cf -size 35000 +oid sha256:ecc5f3820395a67eee396556344663844fb97dc843cda96b2545c954d133795f +size 35118 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en.png index 2c4de61a77..fb5e9a7448 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3c16c9a89228f83eaaa7f9bdd7c0e36a333efb541738bd38411f9f853e20103 -size 23797 +oid sha256:dc0360e2705f3e029c1f8cabf91787a98f7d7b67e6d1f003c54ca4ffd52d08be +size 23961 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en.png index 4194458de9..2a85b2cf7f 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e2de9b362092dbd08deadac70591d4682304c69981fe27cbbeae4f26233d4aa -size 23444 +oid sha256:7ee6f9fbf05261efe52c247da3869c68c6e87f5132c52da72e56cf833ea6864d +size 23833 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en.png index a1b5d1bb59..afd2b2cbe5 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:919595acfee379e3aaee66a3abba61515e8bee35a9fe0250a8a227f772109864 -size 49693 +oid sha256:94014619417d8c530dc80316c9aca36dc08997f516890d7199b24afa46e71c86 +size 50004 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en.png index 665a36ad0a..f86302a8ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:079ef0792efb31fbd773beb77a95bfa713213f8f5428ea1c12060094deac5442 -size 48620 +oid sha256:0a3d3c574589f06729b58775ca37bab9711c5566adff366d4a0196eaa7e10ab7 +size 47410 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en.png index 32d07be159..3ee9db165b 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17cd489adfd7a417a6972f8e36a61ecb2bcaf2a045d33e4d2236004271d4d9ff -size 48317 +oid sha256:277665230a31e1a47489a5124efa6dcaf51a5a442f0e6d4dfb510ff4896c9bd0 +size 48738 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en.png index 9ad4c83991..ba4e6885b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:215e1dbc213d493ee6e24712aa93c9a103f1148a3ea2858ff185dfdc7084ea3b -size 46546 +oid sha256:2044881fe3e54179333fc0b125972b2b410a4d3657faa10769b964e8ae0d2643 +size 45278 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png index b7c62995ee..583a7beb59 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e3385b54a474c6d722106abe9c20eae3895eb6bddbadccf7a1112ce88f87b39 -size 50304 +oid sha256:bd5189b1079f9011162ff7ba2c9211fbfff543db892b86ea3b57ec78495d2019 +size 50344 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png index b450aea25a..1be9bd5d99 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1ef764e86a5133cdfcd2595fef41da81b1eb8bca427c7cda9a5145d3d6e30ee -size 49302 +oid sha256:96742102f57f2e58027e3402ea01c91ee1eab6010dc46ef8ff8ee3175264a628 +size 49259 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_10_en.png index d996699f1a..da7923cf50 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31e32566506ef9c6d34b144663b308a53f893b810960c50e394271ed5dc02fc0 -size 28564 +oid sha256:b1c09cbb0217a1fb844828590e7827c27a2e9a99ecf5aed225f4d1bd0e413c8d +size 29343 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_11_en.png index 9df382c0db..68b16e509f 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6e129a426f95432f31ecb5776ae24e5fed7a3025217c70831dcab30ac270fc3 -size 33329 +oid sha256:6ee66772b20b9c1cacf1f9387c173f8170d0c132d6caddfbd389d254b2ae7aa5 +size 33977 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_1_en.png index 68d54d002a..56510ac1e2 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2eb71eb5d1cac83a03abbdc6ac670cb96993da8825ab224b43e593ae4b6ee8f -size 53719 +oid sha256:3eed90dbcc789f3db882d61a31924668a42cf78e00cd634192a5bfb831f67190 +size 53659 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_2_en.png index f71897a57b..3bc29fbe6e 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71e7b0aba9acb702371ed0eacd6d86735d47d02dfadc67e55079a454c9c8733f -size 30242 +oid sha256:228a9ed83c86a160fe494254ec654fc0bf1d6923e78f5de2b4057d386e25b333 +size 30750 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_3_en.png index 68d54d002a..56510ac1e2 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2eb71eb5d1cac83a03abbdc6ac670cb96993da8825ab224b43e593ae4b6ee8f -size 53719 +oid sha256:3eed90dbcc789f3db882d61a31924668a42cf78e00cd634192a5bfb831f67190 +size 53659 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_7_en.png index c34ec228cd..bdad7df7bc 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1c6a823d353aa2f63633bce1d0336239196f26136b532718fdf2c49c183fbe9 -size 38014 +oid sha256:64c4173147686886d5021cdaf6480644a5a520f81392ca84aa7ab5d705211bc7 +size 38682 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_8_en.png index 651a16265f..f52aac7f1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:359b8ffc79afd24ba68c2e7c13ba551e9d63c72b787e530667cd5de5c16782f6 -size 48002 +oid sha256:c6c73dd018eb82fe9e54ffbe38dcc491938da69791f4ff32081b2dac7549a4b2 +size 47958 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_9_en.png index 651a16265f..f52aac7f1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:359b8ffc79afd24ba68c2e7c13ba551e9d63c72b787e530667cd5de5c16782f6 -size 48002 +oid sha256:c6c73dd018eb82fe9e54ffbe38dcc491938da69791f4ff32081b2dac7549a4b2 +size 47958 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_10_en.png index 12fa559ca5..4d2c26d91d 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84d3be38de70c774b36592543eb5636029b08f9a847566dea4f90979ec9be97c -size 27529 +oid sha256:0b0e1f00b98e463a97c9334da018ba944414526f771aa4ceaea18ab192a36863 +size 28080 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_11_en.png index 543aa41952..a650636f24 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93f49929a2e696bfa8a8938107d3263dcda514f34c8b5eeb6315267126d138d0 -size 32095 +oid sha256:87e337a40741a5c7bdcb2015b8f82282ae69e1eff98e30641a02314660ecbd73 +size 32583 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_1_en.png index 53bf92ea9c..62596baf31 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4701f103d24353378826f66e6d602b0bdf43904ef6b28ee03d25af4a1062411 -size 51650 +oid sha256:66a0dc249d2df7e52a6a5f7cb3f1a94be011164eaf23d365f548b99f093d3d14 +size 51681 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_2_en.png index 68647ba764..f97ba2a853 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2223132d7446121d8bd37b3561fc24ce056aeffc8828b197a2be8c92ce64cd27 -size 29197 +oid sha256:aba88e94521e05328079fb2ac0b06fd97987f81694b3378f02c19a20bfae2572 +size 29636 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_3_en.png index 53bf92ea9c..62596baf31 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4701f103d24353378826f66e6d602b0bdf43904ef6b28ee03d25af4a1062411 -size 51650 +oid sha256:66a0dc249d2df7e52a6a5f7cb3f1a94be011164eaf23d365f548b99f093d3d14 +size 51681 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_7_en.png index d233b2890d..17526d949c 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7aa9c94a5f9c324a3229a5db951411fba6759aee18987d61f6ffc6d74ac79a60 -size 36665 +oid sha256:fd6d4b2d03d0e86a22c48f89f05c83b0e7622f4b6fc2a946ace1ef33e66b2e9a +size 37183 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_8_en.png index 084de2c14a..bf33b63f8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a952926d397cab2a689e3f4096de103bbb3075f27dbe45fff311a153ab1c9ace -size 46181 +oid sha256:d8f9a50b522d34a29ec48fcef545a0880ff0f43d464f1874c3486adb9c79e202 +size 46251 diff --git a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_9_en.png index 084de2c14a..bf33b63f8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.logout.impl_LogoutView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a952926d397cab2a689e3f4096de103bbb3075f27dbe45fff311a153ab1c9ace -size 46181 +oid sha256:d8f9a50b522d34a29ec48fcef545a0880ff0f43d464f1874c3486adb9c79e202 +size 46251 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png index 3783974c39..068a5d0023 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9912a8d83fd78d27fa6b01fd9034a03e50bc598f10330681c51fc776d5541bce -size 29420 +oid sha256:1c792aaf65bc7b87d3a2e7cf034ab7a16e11cef50d1c8d7bc55e319dba8f656f +size 29584 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png index f594f885e0..3a1db412e2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab09bad33db524c5bf21815b0573cce94a5a04a3cb1a7c5eac08788a6fd43bc6 -size 48565 +oid sha256:174878f27d737484cf3121ea7f6019f90098f02007c85c27f4b4cb58c1f9a38e +size 49028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png index 336b672698..03f9cd39b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:028dc1af0a32614235945f29b86fc75f7e4101115ca6bb24a8519d9b540e3446 -size 50076 +oid sha256:83d2c2bde2cee9c9bd385f94131d5f3d13a24b3e8705b4ae8f47ebc0fc551550 +size 50465 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png index 5a7092ba16..18971cfdb7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:050194b3070ce4e062b419467c0429b4ec025bda070dbf9499ebf22094d930e1 -size 43582 +oid sha256:bc6ddbbfa3864a4456e564ba76ac303fc2f4f1362fc710f064f6c72e4b99da2f +size 43755 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png index 8888dbdfcb..b6f510f5f1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:979969357697032a7b7e5e715f42434cee98e6ad6eed439baf79ca0b972cef42 -size 46476 +oid sha256:6e42d39ee8965360d92cb66485670b48c265c0728240529c3d35d14a27f325d1 +size 46965 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png index f1b598d66b..e3b257c5b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6ca9ef59f9e436190c1342dcc0376c88bf46c8b6b2f84b6a487d3bf998e94bc -size 44877 +oid sha256:35262a34abfe83de81ba1000168773fc867ec14a73daa01ff1c40396fbefbb36 +size 45255 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png index d70c672fda..8b0d0ae6cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb661596edcdda0d2a5f57c72a396624ab6f4291d07c0224cdcdeba67fa48071 -size 41955 +oid sha256:2cc504e426c4e96b635736c43f0495d226c9fd8dcdb822f38fe3439bc1478326 +size 42260 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png index cb1af3bb6e..002d56ab44 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:834245a21eb2bb644b5d9afda81346c37a3a6121d20d5c14c562b100cc5b913c -size 45046 +oid sha256:5c365cbee07a0c25ddb41085eb02dddea84560a5bc0bdccf439e80f46f889005 +size 45557 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png index 8b6616fc18..ca3cb0a3e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bb9f6d15e6556e692a6d879202bf6036d9cf1b353f317b911cc7ca379c2a90e -size 43190 +oid sha256:61f557888cff9958c99d1e973e4819dab7d23f0eb261d47a69e14ca9e6beb0e2 +size 43498 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png index 02318d8ce5..80eaada8e2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e01aa32704fb8d1f2b7f4325f94635b345ce3117e384923e8132a68b566d8968 -size 44964 +oid sha256:c478071682838cd4cc7de83ade5de09a522eac5b1bc0261adfd6b4c4caa6899e +size 45379 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png index 9b0b7bfe4f..2b3e376564 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45eb96b553465eaccc2e5081bb8b867a8cd2d4e5ed1ce66767828bfb3b7fc0fc -size 34176 +oid sha256:d2ec2f9105e3aae1f33e98d2bca2743e14d5f9239ba7e78f4f91004fcded11e3 +size 34412 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png index 6170e24768..402a0cfd20 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9a6d982e6a414650a917da3e66b2155f8b6ce3285ca4fefd4afcd0df7f6184c -size 28432 +oid sha256:96ad3fabe9a1753ce36f118c4eaf7cbff8293c5c05b25280f1601d6c006bd055 +size 29010 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png index cb440db48d..043546e64a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06180ecb1a875d6d007a528730d53d4913c733d8b1218ee69daa3df557130806 -size 47781 +oid sha256:0ade5a0171e07299ee3434ce36ccdbe66a4bc5da81409774cfc35d3b99459b20 +size 48268 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png index dba9e6d4c8..a04f0c4bb1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:670bbfdc5ae48ba97911716db461ab391a57e94764508eae88c594b68e96e826 -size 49278 +oid sha256:d5507781d7fbd2cd083f6a9c75118572f269b764da21c91ad82760139ee61460 +size 49765 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png index dfccd98a28..dec09e3069 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0d0257a4d2eed51e9c18f3afbe0e39fffb7440e0ce749eec8959c9b4e5e063a -size 42723 +oid sha256:6b5a66bbb9741274a14afc0e9c117079b4d184623620523fd11af73f592ac465 +size 43214 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png index 4022cb1368..a3529b5d97 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e6093a0a7e2387c2c06270416e53c0654353cd1e348829c31f1377c618374f -size 45875 +oid sha256:bf69710dd5e9926970b9c13ebbe5dbaa9d2e87773ba7c439fe18f3474459fc40 +size 46325 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png index 7f7642064e..a4c1b92d16 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b021dfc67025bf99ca1b6ad8b28afcb29611716635a57f4c3e2b250515f63da6 -size 44357 +oid sha256:ddde9132a3640c4e41927cc53506d0628effd958ab2f38cc93a965469311c3dd +size 44783 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png index dc562de97b..df9e68d874 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:199ae1aa490d6b7c3e4e0a2c4efedc5fd8f9140d18be74baf9b67023f9bc2165 -size 40888 +oid sha256:a228c7efae3558529141ce22a0392944c2475902b217ce11c2217afa136163ac +size 41453 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png index e79cd5a5d5..f5481bbf61 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b5a85043479aff4f13fe1b2e86ad4700410dd9c57219659c0b2cf93acfdc6a2 -size 44679 +oid sha256:e67888597de49ad6c67fcc4bb0e31393bc0c61dcbfecd8efe01ea51b2a067aac +size 45073 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png index 4825735975..7b830f1377 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bcf537c2f4d01fef81785ddc0b8f5133e2bc1966e6837e239ccfc0a06f14d18 -size 42111 +oid sha256:3b9d919df0d86bd8e3b177bda45084b40b89803a4310e3782e50ce19ce00fa24 +size 42624 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png index 225a8b94d7..7fd5f1293b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f27444a111e07658820b19dcc780219ea80b2ea5d01495ecee1255c0e84650e -size 44507 +oid sha256:76203bf3973c8a5234676309fdfff32cb312e2f57e8e63a99061291e9fdd5219 +size 44887 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png index 46c4f1942e..41ab0f2483 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbe4516b52f897a4f91d229f3a0f1bcd3a867d9494ac894d11452bcdf9407f2c -size 33013 +oid sha256:3b653b0dadf9e69b138bbb0d51e4e2176e0ae6db953056daa0af1559bc747fac +size 33495 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_en.png new file mode 100644 index 0000000000..148289d4dd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b64ae2fd2462ec3b62750084f47ec29c984ef4279a465b926c9d36e23834092e +size 328321 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en.png new file mode 100644 index 0000000000..21d03570b7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864883e9220a4d653d2d6b555ace89c77891a1ade04cd2f49d89c8bb765c6774 +size 282733 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png new file mode 100644 index 0000000000..3192cf2a5e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1885bbe8b0ea497ac20d7bcdd0c6a93ba0533e898378d35e1cafdc8c9924fd56 +size 279984 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en.png new file mode 100644 index 0000000000..1857dc249d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e95d7091485a2e5bb3ca8966c8fa93f21e2e5fd2d939e7582224ebf32c05b619 +size 277160 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en.png new file mode 100644 index 0000000000..25a72fa80f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ce3085cf45a94e014c9ea79a46d47741fb098853ac81e7531fdbfe32e8d6f5a +size 251213 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png new file mode 100644 index 0000000000..ccfdd7d9ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe6cb72de5079be2ac02ec39f74bac0d20492cd6f5509b71ad9cfc4cc52955d3 +size 326468 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_en.png new file mode 100644 index 0000000000..374a59ce84 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:846467c023fb2f6a091c6d350927c2891ddcd164dace189d157a8570339e2d92 +size 282932 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_en.png new file mode 100644 index 0000000000..b7cd1d9387 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_7_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91472488cf265e9ba62a8b6690fa34ced41ebbf7a4668424c02464086aaf9f9c +size 283181 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en.png index 586bf84274..91c617002c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22e3d682c4866bd5c519ab88d08290708929af805438a0bd093200cddcbd41b2 -size 399376 +oid sha256:14ad2df78cdd8422279c413e9ed2f709636d09cf04b3ee4ada5b6b53e8e42c8f +size 402328 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en.png index 27d8600ab5..74b24f7194 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02bb9e9de3b0ef480cedbed50483bdd3a899497ecc40ead72491106c6f6b6611 -size 399030 +oid sha256:45ade3f4baaf0b91300a6829fbe42818d754ccca64b1b090195ee40eee744ee9 +size 401973 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en.png index f1cde998de..f229a2a82e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:947ccb947f4a961ff7d17b936f2c866d66fea0361879d51ad4c65d18465c1a9f -size 59226 +oid sha256:4ee2ee54c694a45316bbdcfba7038391fe808f5292356748e9e66f46135a9ae8 +size 61457 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en.png index 586bf84274..91c617002c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22e3d682c4866bd5c519ab88d08290708929af805438a0bd093200cddcbd41b2 -size 399376 +oid sha256:14ad2df78cdd8422279c413e9ed2f709636d09cf04b3ee4ada5b6b53e8e42c8f +size 402328 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en.png index dd5a1e333c..09e08632b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d9e221ec2f4ee764967e94c32f52b1615b25dec8fc7697dd5bcd01fc4da8d69 -size 59098 +oid sha256:44c7b0e86781ff6112c3be5b575b548c5ae6e9530f6f27a070ebb008d606a20c +size 61326 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en.png index d695d41c94..171ada1369 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:547bf4e3bec05c219f5a72cfd8d506eb7c39429970cced5c2b8f2999ae390265 -size 86149 +oid sha256:c6e4dd5eae99a0454d6918b65baa3ca36c2266abfd9fb37dda961268e55827a9 +size 88994 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en.png index 4a6e1cd828..ea4995ba89 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d326595038160376db620a07a180de7af37ebfc76d4927ed1176ef6f4370aab -size 72700 +oid sha256:0c6a5c845db0d9ffef0074062cea67134bdabbe91fd1826b2236a0f92e4a6dff +size 328222 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en.png index fa4beb6dfb..c21cab9cb2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8e8bcb6fdffb2d8673e4ee4e16e21672ffe99e717c48fd35b8411a8aa0530e9 -size 405064 +oid sha256:58ba9fd5edd2aa366b62a0a8880b6a3f6f7142861180c4109ba4904c43101433 +size 75343 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en.png index d789e92297..42cf04b201 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3caecb171d3095ef5c3593c6289b2d99e6cd6a6c635d822ad24061e502bdeb2e -size 82790 +oid sha256:43a670de0512321ca3e1b5bf0c967c6bd8504df542781c4973c8806b8b95abe9 +size 408029 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en.png new file mode 100644 index 0000000000..84cc698a63 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsPreviewView_9_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c0ef41360eb82092680b5b262bf1ef8449636387124c5aef940a727b3bc6aee +size 85137 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png index 5f51b27f5e..693a36b7dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3274314ab9e39c23a6b9d66adf80ac8bb7b364ce60a300f96582374a18bc821f -size 54590 +oid sha256:3dbb8ef39b6a7a0c2f576d8bd101aa97c89da696f23e6d5836846494471c01a1 +size 55799 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png index 2e70435fa5..d3f7da6320 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a3667390d9d88894bbdd5d72b0b714b29c9ff160653e310e97b0da87b6ba8b0 -size 54724 +oid sha256:8a94a4cc176b5d52ef9b21339749324adeef9857b6cdaa2ae973903e2408662a +size 55904 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png index bd3b0196d6..0f6de1b844 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f7f7415abbe51858f938e16ad03c6aab983fb3d5e2a53a028bae3c031446646 -size 67848 +oid sha256:28512b0fda0b71b98eab03cee15c9d3163825b1a3545670cfa277ddd5434c87a +size 58417 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png index 02d0fc8eeb..bd73e322ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f77d8243c23d3f37dff86dec53671cbde9fafe1314f6e6b445abdfe47d751ad6 -size 56187 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png index 93622cedfe..bd73e322ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87193a7c9eb9d40234970e5f5be02e80f2b3ffe91e2c9ca6afd58b8bccdd5911 -size 55075 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png index adec213a8c..bd73e322ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ebee4aec3dbab6e1bb05fda7c5e423f0e931560743d0f0ccbac6b70fa9ad808 -size 52921 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 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 986547c8c5..ed3c8abf17 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:d2acf7cae297e8be76765b392dc07a36692a9e597b8f205de31a04dcf8b6bdca -size 37918 +oid sha256:a1a4672f87cf79f8052e29650be198bee1ee6c00a24ded5fb243b6a32b51c372 +size 38017 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 d333d2439d..6798590348 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:47b4d158df0f83631d099c072b8b3d95a57d539fb9e8d7e6802f6a90a3b78284 -size 37904 +oid sha256:f8c69700f738d00379102fac8d2d1f33891983558d4a650edce317282e16b6c7 +size 37737 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png index 303a32a04d..8f4e2dfe50 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36926eca7b934f7794ddd73e7929861d8837e9ac51341520c883043dc53d1927 -size 25026 +oid sha256:bfc50e8997f52f2b0b808af90b09000a27c9943505460692fbe541a3bd95c440 +size 25622 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png index 32c4015a76..862e26b1d6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:493ed64dc981851887506ce33cdd063a107c8d5e6376027e7949a248b88e2a42 -size 24167 +oid sha256:4faa8dc1a367649a4a862e8647e733d77dd31737b34f9a8a5997f2345dab50a0 +size 25035 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png index ac1b698ddd..8a295a919c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70b06a4411aaf9fccb7dbb68878ee4a01b0987fe592a1aef6fe474dd555dbd0b -size 42536 +oid sha256:e28b5fb8181f780a372aa944291ef23349f4acc3460786ee152d8e281e85b866 +size 41993 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png index fc55c74f1c..14e06bbe27 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ed9d6c254ac362f84b9be0761f0bbd7aecb14343a131d54da4b2be0ad033f87 -size 41165 +oid sha256:d20f6311164e291ae60542c2316cb4d438058322fca1b0db0980f0579874f9c6 +size 40650 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 index adeb4f7c4d..2810ad22fd 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c02999b2d0eba92f1e9b1f5fac52bcff303401f177459d1a439b7db906e5c2a -size 64622 +oid sha256:3bd5c5c53110eec56892fa6f93ae8727104d7eafef03e669181e0465096bad94 +size 64980 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 index d89f062c11..ccb7dee9ed 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7f6b64bfe0b47546a009efe23060ba091adbad32288ce546cb3b030d7a9ec88 -size 66177 +oid sha256:e3354fc31ed4408c4b53b63f04b6faf3c590308b9c45e5a18538b0a9cdf7961b +size 66453 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png new file mode 100644 index 0000000000..08169845d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86eac241e9084cc830ff13d231908da2eb0dff246aa5cac64dddaac68a5d5f13 +size 295298 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png new file mode 100644 index 0000000000..3f12875a5b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7076c9a44a433cd9eeca536365ae262d39b4ad99a8f854faadec272cb1b82031 +size 294992 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png index fc985de87d..29e7582971 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f358da2b9b0521c14e525bcb2bd41c29c96aacaf5f007546e6d362bde2ee3fb -size 612526 +oid sha256:d6441c572f64d99bc119d649e5e6422fe516b9d097f310ccbd99a9ddded9609f +size 612494 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png index c056ddecb1..e76e0db72d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcd8208f3737b5b5949af591d2417d16f2da18609f3770201f2e87577b250b2e -size 8947 +oid sha256:45b2370c78dd907e5bf7c5df18b98df636e1099cd1de5b54923f7beb9bd17ccc +size 8680 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png index 775c2483f8..429d9d0355 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d114f04976fbce895cc53c2c57df8af1e968067d4ed451c4a11b00ea008e14e -size 11127 +oid sha256:cbbbc103c2a7b5e8d942e0af113568c28dd62863ba0cd1dfb7a5dc9de56b3760 +size 10863 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png index 4854996fc2..c45bb0c16b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36487e20168db4594c519e4a6901b18d0e10604aaf10f07f9aa95d44647bdbd6 -size 20287 +oid sha256:f758005da7c969bef056ccf52d27a099e3bc5e900643e50990be430d640edfce +size 20031 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png index a27104d999..faf7f18e65 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3576dad3e0443c638112c329854fc4d8818f2045dd2e17fe3a0ccbe9a9b1394d -size 11048 +oid sha256:c43d85e4658b9fb930ef76258479a876d5cde3b545bc4f4a76d86e4ef2d7a82f +size 10773 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png index c2273c12ee..f8f7b243b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b4dde4370b8bd6d2bcd8d3c9a877595661ee1b1296dec0f73b041fb556a3921 -size 21282 +oid sha256:c836c0a00fb5697138349028540d5e1cb4f1fddd2fd50d388cb5a1b55ccaa8f4 +size 21023 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png index 7fed1167fe..ee33147a2f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc2da3562566b433c6fc5dba185c13127f86890b26644e41d9b5c88f1d130fb3 -size 8671 +oid sha256:3c5351b6f9f14198aec7bb4bd6353b43bbf9f487e192a5c927791756dc354ad9 +size 8431 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png index c74490bfdf..802d8b0445 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0870cf3f791d9a1f5203e4d81c01fa986a03a5fa44589befa9f8163c834637e3 -size 10799 +oid sha256:92feff534e8ae02d574f5bddc927bf642e3199dc4f2d89a584d087058d18fc01 +size 10565 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png index e3f7918645..013938783e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2031bd6ed35ec1de5a17d1f85aa52c4cb62278e56f1ec5df48c0afe2ddcade38 -size 19216 +oid sha256:2dee0c45a8848f8771bcb0b60f368ccfef29beb7528c227a1a9f07c71a0457dc +size 18940 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png index 6ddee33e8c..f5db366e09 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9cac5229b5129b2bc21cae8ead0e7d19b58922056064f28bf0f59074b4e21e6 -size 10742 +oid sha256:9fa672f2cb7dbf0fff171d507e66d1ed8494cbcd93b48bef1ee1b9b75d6717e7 +size 10499 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png index 826b4193f0..2ed11e26e2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b693ccea2549b9140beacbf2ffaa8ce409ae7c7f2f9cea0050e56527c2c03fe -size 20380 +oid sha256:5735813e0f9c40e5d861ba0c35ba73689ff7ee41224829289ce017746323922c +size 20150 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png index 357520ad31..6e88f8cb19 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:043671b3e2176b35ab9519be38ed5711f3eee99a8583583c6fde11f58250aa48 -size 8251 +oid sha256:e155260fd46cf1550c823e2b0a51f3f6529acfd131fc73a7e66dcaca24f3449e +size 7934 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png index 36d8c49114..65beb3b8f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd96a7daf5c19176ef997fbc80a75c3eeb19d470519ae1d57027b5f388736b1c -size 10526 +oid sha256:49a670d25e379b2385ae14a814cd4aca9a0bdff00c3d62d31b146139c2d0edbe +size 10210 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png index 317cdaa271..33772989e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bba55beec2451cdd8eb5237e85ccaf37d98ff86435015ce019f8ef2f131fe31c -size 20520 +oid sha256:d633e28eea9674914bb018b113313c2e3aed6463e2de5901243f5432087a3c1f +size 20237 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png index adf5f2263f..c65619f05d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:207dd1bfa213f3153cf2096d6e961d4c1048664fb2bbaea546af843bb96769a4 -size 10349 +oid sha256:10b79226a4eb5c67afc1fc8ace8c6fb49429a69b1e1518b622dcfd20bc2820ea +size 10032 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png index a2ec73f85d..d68ac1725c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15a00a56cdb8156a78905fffd95dc5196f4267768a60a08416417322ec6c9801 -size 20640 +oid sha256:085310083c4150fb70e48087881e529f17005df69cc01dd53807a06266f4f4bb +size 20317 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png index c7503a4391..4441c3db6b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1b8818e2f1e5f822b9560ec30297460bab89f05d94e36a989aaee4277ae1216 -size 8071 +oid sha256:555333bf67a1f4460c0f0d71393c03914fc67cfe6f4e5161468aec1d95e587e2 +size 7803 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png index 6a70eee4fa..2755d2b93d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f3bbd229376c0f2731f93cda00e4580c45971e4ac07bac7b2578d45b48ba079 -size 10220 +oid sha256:36009b81c172786ab1d87834b05550af706b355f8d044d364b1f15e58097c8c7 +size 9945 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png index febf7e36c2..5c5063083a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2e3ff1bf09d9e616862c74ef29f07c280bf07af8355eb282e51745e0f6927bb -size 19380 +oid sha256:d40a8265e7bffd24954d6fa3194c8c8c65b600518c23d6089b540adabcb751e5 +size 19141 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png index 5e1065d2b2..48242a7b75 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0aadd6c4d503ebf0224a941c798363e3302018fcc9e726bc31808ab7c5381504 -size 10130 +oid sha256:4ef1884c4cac3debea52affbab071110a49b40cebfd38b316693085464fcc440 +size 9859 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png index a9fcd11f7e..3146070439 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f44732ffe4caa44b3fb62bac8a6da3e15a7917f0918314d0f1407b08c8b72da -size 19820 +oid sha256:983620627af06dc1937b040fe21b3e12920e84116cc584fe76a7ef799473540c +size 19547 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en.png index c11e6b978e..90adfc34e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3daba890afd2533e1746f1ffd0c535674a4a238e4591da2a6bcbe31716d26858 -size 143676 +oid sha256:2ae9812763faa89524be29c0b796479c51092e570d31667bfbb7209e901dca83 +size 225821 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en.png index bdb2a689ac..eb605cdf3c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82ed52f907048490ffb63ea9b33274703c1d92d4131cca0320816cc6949bea5e -size 113727 +oid sha256:ab6262a03bbe7e4ff33ad1c0655652b58a78cbf6aa0a2e4a8f794f7b347cfff7 +size 197616 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en.png index 5d9f6ab2ba..c6a916891b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae3a754c163f83e69ab062c8e638a6e620948081d70105ddb00c32dda7c2ba74 -size 119927 +oid sha256:459141f4442bade537f755d56fe0b4569980a26b92f23409c477802462084b3c +size 122118 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en.png index 01d01e6806..7a2c4252a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:757d555f948a637952aed4b0ad2745212f6a9ab279d172b0a1649fdf547c5a0c -size 120027 +oid sha256:1a8c03a738592d5ad555e1cbd07b5169edc45de18044dee9139dd15763d58468 +size 197204 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en.png index b9280ec225..179d838b9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:463a2e544b2806f4082c813ec3ba8c6ce0ad0ebcd8ee32666806757a90c248f5 -size 57131 +oid sha256:d761883dee687ae4917949d64b5f6fe13daf1d54ce8ee05785967c4906078560 +size 80994 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en.png index 6962ed5f4d..12c1e7fdd5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb0d86877e5d049d618836846cd8ea97b40d1d66e7bd8d50639280fb3e829372 -size 38499 +oid sha256:848201daee4c16c24dd6d29f6244ac24d197832c43168bb5d76245ebdaf0d643 +size 73421 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en.png index 7b0ba78a2c..140a3c3446 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b41879f5159a126b0641c66bed13470756a914cc038dd239d40d359661784b71 -size 40696 +oid sha256:c5a9f9104c9544e32ab0ab67ce03dcbef51e8613c1444aec866ff9bf60f6d241 +size 41683 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en.png index 8ee52bca11..78bc8c65d4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38fa52e6e755d86aa79a029427f04146a3939ac2038cc15717f5aec84ce3be20 -size 40870 +oid sha256:aa9d77577784fe54259902d90d447ca253de22c2a9f8e62d767456a5d52179f5 +size 73006 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png index 804455160e..276c82c9ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ebc7039d4c3db7d0a7cf36b7cb317576b9eebac16bd29fc542193ddc78a3467 -size 46843 +oid sha256:35741e180e6c07b452c485099c11473e80ac308ae81d3620fcd8e3bd1385f977 +size 46801 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png index e913391bce..1b0aa01151 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beac2c2c811c1fe6e8a1ec28fe7b17e9ea4dd6641d9c79d0b7c18ad3ec36a508 -size 48920 +oid sha256:9172fb348403a2f590f2a3fd522a9113e5a9e1fd06fae07a82a453067c5c87f7 +size 48876 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png index a73ab70bbe..7072bbc0fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:737a2a37bd4b753981d719e70c366688a92ebfca62e53a1e60ae81a073c40f14 -size 48858 +oid sha256:b3e60938f89fe4e56acec088683232dcce2b13deaf55111ef3e049857dfe2585 +size 48824 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png index 05d46d854c..1ed803b334 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:087de5e8ccb4d69ec2c1f12aa7c64876228c3d54256852c7b01a4944a7f34e83 -size 48929 +oid sha256:997f60386b4fcbd305aa76c71c95d50860ebb276e6977c8e9ad8d3b49ffd5d24 +size 48896 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png index cdf797ba54..85edbd5662 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:effa17c9dc842c49c1b67be244f0c8c53e859fe80dab54a373d7536ce3898e7e -size 45498 +oid sha256:9b64a00c01448b8935985343ae33912c80dd84ea2329717b8addfc07dd6ef012 +size 45403 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png index 8e1812e454..6fc78b1598 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2b5f85b44464923de745354cd0aa028d94e235b8797207e82d8651a4281db87 -size 47662 +oid sha256:e4bb7cd6b419bf262fb959ef7062d842a9f58f84d85e4a928f11187ba692f739 +size 47590 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png index 1f3b56bcd9..1909c9455b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffda5057a816628a946e6441a90c47a469233a61d3c8da11d39797b8f44adfff -size 47649 +oid sha256:2aef02a3fbb01e4dc03179914298aa169800e2f866d678ea8fa53a492e5e451d +size 47577 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png index b733eaa9e9..69a497b6f6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbd2c72e13e300bf3f223518f82f3573d70916ad0a274a94cf62eb4220770c75 -size 47741 +oid sha256:be08acaff0ebacc3a4158489c7bb7ce944c8f0b9246ef6d37bd58f5ce7519a94 +size 47670 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png index 80857358e7..f792a1663a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54e71f45db31e8664c42f8c05feb91ae839589e00cb89d12088c15c85fb9f25f -size 607824 +oid sha256:e3bb005a88e9a4d2bb850478ee8ac2c4f31d32625bd6d2abf455fade461cd399 +size 607806 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png index ab421633db..284d163b99 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35e02daa2a1f36463d5d0acdabd2b8ebbd972ea93d171daf628b643fdc995f3d -size 4653 +oid sha256:bb2ac7a95a8c022d73bc542e379762e82f3bf29ec92ba28c9c6db5ef75892862 +size 4650 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png index 33472fbe19..4b55a4723c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f5e23c5ccacb8edab5d86a1bafc482af9adfd734ea845585ef4f7c4ba52297b -size 4365 +oid sha256:ded635195c57d4b20a2aa8925adc2df072ee0da7c4faae0f360f16c2a6eb0600 +size 4361 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en.png index 5815548686..8d62522c1e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abc1526f441c218d39e44ef3f146d4fee3bbb0628c4ece25fb2ae4ef7e4100b0 -size 48820 +oid sha256:13573edc4f14581b14a0238a66b6fdc040ccc02d49cd670c5e0ec1eb78b61bed +size 48621 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en.png index e01465633c..c61ea59d37 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20c52bb8f44c186d3104d457c5de9c0597d99f0a5be37a9d8680fb4be35d422f -size 53777 +oid sha256:d3153640c7d5f5be8bc66424cce167c44f48497bf5d91a25963a1e2d5e506d6f +size 53643 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en.png index 2faf0a5bb2..e1f1510b5d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ef807d846ee06add8438e81ece8570cf688742268af87b6985551c3cb2a0faf +oid sha256:4bb4d59085cc26a284e1bd9c2e6fb7e982a1b32eae5f46282a4dde93b5d5ba1f size 5735 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en.png index fdbb4a4aae..b09b378502 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46b2bf1b4ed49f02ab2ff5218ad6bf2a7e0184e022af60d4cbeb8599646975c8 -size 5670 +oid sha256:e6cf51de97f040b1028bc32d167f7c4acdbd2717c3222e1a1d97929d666cacc0 +size 5669 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png index afec980e10..510521faa5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3624fe8448ae4af2481e2023a978ffab2d69f2784b7cef41e5ae2e2dbe8fdbd5 -size 17804 +oid sha256:aeaeef2348e2844e300c83b257027468ff51977bcfa2d6c540f8c882d2624826 +size 44173 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png index 8f7f14e64a..5dcea94a33 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bd02d39619efbcaa6d96f1e75a0d14a572c43e60bad0c3f84d6a5a48b6fbda1 -size 17395 +oid sha256:4a6c9b24bfc9159fc70b6431f106836031c69cf5308674c20d88c1570f15d105 +size 43144 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 0435f55de6..ab6392a424 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:ff839731834d7d0c94fa9a2c412014ddbaeaa053c5af1c8cbd00a3f3a583f364 -size 379511 +oid sha256:94f89a37549a3f198442db467bc7d8539f0b5649d97927bfe731f090d6dfb03d +size 379505 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png index 592d4b0e0d..a0b392a659 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87b78e2a06bf592ac538eecfdca70a9c5ec51f69e397623edcdcad8daaa9bfe8 -size 293405 +oid sha256:495951c4763e1c3015a88964683d384b84dbfce1b9569630d81ff8578a7d13e2 +size 293410 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png index a84752536c..97826e6a68 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bccf822697a9710752ce5323c8b6481d7080d9eba7579d9679f451818ab71a6 -size 293202 +oid sha256:25d4838bf2587c2ccfffe83d53d556ec35ee7691e6eff91689d41dfcd538a324 +size 293209 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png index 4faa1f828a..90e9e72d8f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6982eb3368779c8e7fa602059d9af4119f450f9f2f7f384b0e1251080d286c1 -size 377103 +oid sha256:7684bb50d0012a7175957d0bbac28f68856573cea37f45697ef3b81235f1eafb +size 377102 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png index 14d9ae5907..c7a8f44df4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:add275ce7641fbeba7acca740b2d0d6df0ed06c1c3c5cbcd05a1ca474b086ccf -size 375878 +oid sha256:f6677e0cda28bab1973cc767606eedf86eae3a1ae111b552c6f284eae4fb2de2 +size 375875 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 3ac59caeea..1f0fdabe6d 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:1a94d5198e27338675c0e15df5874ba3272ee315b8d77291dc7ccc7c754ed1f4 -size 365082 +oid sha256:b8bb5a6de6601da8d7fe59f110a054577e2e25dae4bb449f60c04e7d7a1b9551 +size 365073 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 86018764ac..195aa04fe2 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:8344427a0fe87cbf0883ff53a9ee4dcf90976a7011f6618d85961df48b2c53a9 -size 370228 +oid sha256:76e5ae44362792d570cd82f3910c8bd0acde509b8418c5892e0385e7d636f042 +size 370223 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 94428abb56..84d9d4e5c3 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:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 -size 347897 +oid sha256:7c609a633ab23eae5729ecb57a17a385bac65e81cbf0180c01e0847f1028039e +size 347890 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 7ddac02717..73b7e674e2 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:a4ac79f3b8fb4e231244bd96b244a4f2526d6ed28d558f9d50734d83bbb74992 -size 363909 +oid sha256:991deacdfb7bc371a2df960a2364ae3b2187e588aa5afe7f39409e367040dd9c +size 363893 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 67490ba700..01d4dca84b 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:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 -size 369730 +oid sha256:ef2bf449671b652cb152b3baab92ab100eab77a54229ab49daeab156f2f484d0 +size 369724 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 68f8148879..b5dac8434c 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:6ca8cf82aa8ab1bdad56a27584b27789402aeb7ad8fadd038280a376fb75a72c -size 345094 +oid sha256:1a92f8af94682facac66d4aacb6e49f345561c15be2f8b689041b78aeaaae831 +size 345062 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 14a40a5676..a332744358 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:a11044fe90227f1f316a1ade8587e545bb620e11cca76865fc2eb83cdb817fc5 -size 364655 +oid sha256:588967869cbd25ed8b82aff5618175660a8a9e45a5a861b1c7867cf1913f575b +size 364646 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 133fd3903f..d7b969fd5b 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:426d1c844a8c88281c5fa4c61916d93278fdfd38f8cdf2dc54bf1a821f2de3eb -size 397167 +oid sha256:2528df873713755f542f61cd794080929586c1bd5f316c0378ce9e837c21d50b +size 397145 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 d235670def..6e83d6491d 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:a00bd766a00b7e7c6615fc53bf469b3ca5056a6c514c096431ae38d99d604306 -size 364845 +oid sha256:34152c742abc486a48234e22101833bc163cf11979890d06aac41e68e3c404d0 +size 364846 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 b10f9741b9..0021619c44 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:3ec1b0244dab22414c6719b3309426e13128d0c70fb67dc66cd8a60d42521fd0 -size 355411 +oid sha256:7b23d3a37774283067a8919b8c332b1bb5a79e90dceb7216b484b985491282e5 +size 355389 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 422c324695..77abe20982 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:a1df8d22766981fc76d6d767dc3f40d98cadc7c51be20417b067b91af604232b -size 367884 +oid sha256:5c3d87862c6d7fc88d0f23cd54c5b9f77028992fdd7c282a195b964dd17e85ac +size 367855 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 b6d1f07279..47a9a6b630 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:4bee12a6bdb18fea5ac6997671bfcb903296da7278cf69b2456330066ca31dfb -size 342870 +oid sha256:54372845e922d047408dd960d048b9b5eae2263d589117d8e7d197741aa34f32 +size 342827 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 e9d85990e8..a431a9429a 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:66d4b6ea4a697735bbb0fac936b89cde99c5077973ba828894bf50b5dae233b3 -size 362848 +oid sha256:93c2df04d65f6078ce3f7067fbae8a7317ec42ed0fc013dee962a3953269f3b7 +size 362813 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 0eb106f4a9..b51a89586b 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:217ac95bf476c53d2dc6d3d0143b21312bf38b8ab86c8dfd4f9360582b584f9d -size 395476 +oid sha256:e642229c58169775fe702769a306bbef22698516ae6f7ac75a761213c3f195fa +size 395446 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 0cfd1dc985..1a8ea3e4e9 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:545c9e8933e420cc3f8fbba0fbd5162c946c65a20721faeb77c568596ca720cd -size 363045 +oid sha256:66aba7b4271a990fac85b0bbc7cbdc371fa1e38a4da5bb4e2f62a80b21cb3a8f +size 363012 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 c8306263a0..c9227ebf94 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:756f0cff90ddd61adce2f467c0384d74d430f6d51c091e4980e45bcc9ee6f0fd -size 353705 +oid sha256:e614f4a8657637c584a283d69124236d00fe1251c2b8d739c5688a57a6fd152e +size 353693 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png index 480ae88643..cacc88a34c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2476a6e3d5beff4360a349adb831e63fbc05962b57bb0f551ec784e28b9495d1 -size 411656 +oid sha256:789dbfa4886cacdaf5bab7bd6e501d0dd6e31af965b398d9f61d7e46c653a8c9 +size 411661 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png index 75cdc28a3e..e85c9987f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cb2e25a8f461fbb9ca9082bf94571daae1d2c886d2cd33ecea14f955eb959b2 +oid sha256:a740b9d1af8d48d5b9f9bd47abab34ed37037c7c2646d92a05af139435f23d27 size 409955 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png index 22654db502..c715905d3d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:341788fac989135f221178271ebdec2f18056780388f9413a3d3ed12904f28ea -size 54429 +oid sha256:f7ca58c03dddd32c419bdb5419767f6d8f784256fa957c2599af8311265fa959 +size 54305 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png index ce251301d9..fc4d606c89 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:877e252ee1e0c35efef1854bf6c94783add9138148a9e910bc7908ee7228209b -size 88375 +oid sha256:41bc1773dea61705e9109be0e4570be26e3a88c2347d028e348557694e843a20 +size 88182 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png index 72f4fa03b5..39bbd406ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a0827eb23b48bd03fde5078598cb7827459a213ff0a693890581628330f6939 -size 66845 +oid sha256:6f7e3f1cdaf18d7d529be07cde3bdc9100243780101e73dd53c3d78639775974 +size 66862 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png index 67aca77a8c..414db592da 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c94c770cbd1f41f0b50d0a64290c96e1a75bdcddbd0f850d097cd41e5f2a9cf5 -size 493537 +oid sha256:467e5aee42e9041db2672dc0d37e98fc1090c19207604a31247f3142d845f792 +size 493534 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png index dde5ade114..b9723dd5c4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d73c338dce5b27c718cffbbf7cb1fe8f5b9d72f0d335afd81526849190d4941 -size 488704 +oid sha256:723238ad84c9925eb0edbbf225b50a03bf29e5671777d9f03eba6b910ee00fcf +size 488705 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png index 482736f6a3..193049ba0f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31b6fd539808d47a692e33948145732b530e465eed7356e3a47bf7da157dcb18 -size 66920 +oid sha256:80436cf3797f7fe641e3312074d29d5385d09e93cf58927702cd5a2d9bda6a40 +size 65249 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png index 44091f5537..91a9536188 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea2bb433c696a382c2006381c987eae227b72a159d03d3e654397b00c909f203 -size 82141 +oid sha256:2928e30fe5c473b93644cced7f8826696b6a8018b10ab96d89152b76c835b006 +size 80674 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png index d56d0cb66a..c19f4e5cf7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50307dd635bc606def8fe4b80068b7c209e17828718c7a5992127953226cd4e2 -size 71172 +oid sha256:bda29daa05fd2ca70ee96e430e887c02d6a05e62accd298b0206dd1c532a77d3 +size 69548 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png index 72315f46c2..0b4707fc0f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f0a63b3877b4ce2e7613164e9103a9ba935d64886ccb18646f51e0fd9750885 -size 100976 +oid sha256:991669a0939c78b1d1238cdc5f0197e123e5e21229332c53a7e0d65ff5f11ab7 +size 99624 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png index adf8d255e3..77626b47f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8fa43d296a984d242ed56f7879de4c38f3da94147c365c424248eba2c4ad61d -size 371263 +oid sha256:7c07f6bff1e3c3626ec2bb5ecbdb3f17931a8ba1ea01e93b6d14594b9ba11639 +size 534417 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png index 80dcc6a601..4819a978e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e5dddb25a8a201e5334566c90a252111e0d8a65c9949e4f0b7d3d66b3a4103c -size 83890 +oid sha256:acf5a1f1e3f1073fa1cff4dd18ce13b5ec6f19c57e0ace95b074ad1681813fce +size 83691 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png index 3ad7246822..e5795401ce 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bea070091132156a809da1c185e2f94333e62f8ea9ae7d6d1b789adffb869522 -size 55538 +oid sha256:06e2c25d3cbd111b22bd0188e2b1880729e023170c7bed972cdb3c34544ba55e +size 55547 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png index fa93db2f0d..c418921834 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c57dbc66342263c4ff1d47c18099dd2cc22b752f1b6bfc853566706dc0cf5110 -size 486435 +oid sha256:3bf788838a204acb53bceba268327e3c2a772447490a597ed4929487716bc6fb +size 486425 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png index 150b2b0c87..6ab414ab0b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d4fff1585f602d561a98bfbd907d3c7499e6717f488eee711e90f2b0f5df7b9 -size 481595 +oid sha256:80e198bd8cf523c785de4a66c4c22cecb22d1040377bdfd66eeed821a4281592 +size 481590 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png index 41d2ac5ad9..e8f46dab8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bfcf8909d3414e517357d82c203c5464071b3f673d7623d4f75b6c18f25e22e -size 65742 +oid sha256:08c51bcd425352f667189a6fdf98067322f90f76d506029f3d8db640c943654f +size 64109 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png index ecf72ec264..a987d5874c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d3d27b7a366ac20b287f7587866dba06dc5667ba8cd3b6dc040d8ee7e3adb94 -size 80499 +oid sha256:31b76164389481cbe53ea7caa1b1754202314274a0634b8737ad7023bed62a02 +size 79065 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png index 17f8b1974c..837304dde2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdf2031afb6d849812555f8b38f6b0f1f65aa959f3cc9e468fdeece65c150858 -size 69980 +oid sha256:d9962689d7ea7eedd28c7dbec66244df0321bcf45238b47fb9beac50ee81b4cd +size 68585 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png index 780f2f9094..6130f85eed 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0034ff36f0d58e30ac75b36f0869dc27f617d9f1dfb1cf00b88c9bf32d23986 -size 98632 +oid sha256:58d5217e29f95074a403572449d54a072d5969852d09c3a1ddacb43949f4a570 +size 97346 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png index 7b5512dcc7..40ba1155f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bcd9e0934b5f6808d58a3d3506641f173c4b223d78c605de09832d3741e8834f -size 149300 +oid sha256:122b56f230efcad08fef31e50709dfaf4315befcc119e65c08b71c52404d87c7 +size 216117 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 b2c131b124..3ea918900d 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:e26887bd81e10726414e1833029b4b51e22e534684a230777ca50f86024af994 -size 56430 +oid sha256:e088da462be419f059caa750afe0ce5d806ce25e682cb5aaaf37f88e5c236ee3 +size 55745 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 41b8b16bcc..695f6809c7 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:ce44cf850169736008a3f3fc21a2be4fb044badfae631b0284ce379b325879df -size 55533 +oid sha256:f5658c53966bfab8a24a1e123d10a1b3c98b7d43361469ba8d6519c2d2f1cc22 +size 55067 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png index d4bf4bc165..92a01e3e98 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:397966acb792f1a20a79cb865dbf38687244baa9dfa3bc6ceea74919446ca3e5 -size 32476 +oid sha256:8ff566d11223b1a330174a0200848442b510aa378603a79ec5017144f5b8979a +size 32743 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png index 58e0a3469c..744713a8c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.topbars_ThreadTopBar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:434edcb217dd174b57435f1ff2f86cef8ae025ed7a058da229e1335fc0e00801 -size 31748 +oid sha256:1b866f67d78b4add03de052d0eb993e596bd8afd6a77f4cf27ff7dc7c17d8c51 +size 32263 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png index 59b336cb1a..631559cabc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c64b270815016ecd157fe430538090afc8f94c4ec2402a6c6de01ff1d762db94 -size 131744 +oid sha256:ee269e52306e048bbd0a06ea7c56f9080f37f37b00909c3d48a02a3a37bd57bc +size 131800 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png index 9c5932834e..5f724184b0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0dd30568e628750f922a87c2df1fd7751e9d899b9163253c9f2a0c7b03e4a869 -size 56247 +oid sha256:179b30927c3db3963bef12243ae69908ad0b99275906197abd02c110f012213f +size 57267 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png index a63b53d2c1..157e74f7f0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7fdd2e68114457368c5d19fe117b2d5a86a02ca475925c9fae0269ff92f5144 -size 66312 +oid sha256:dc02a7c7b38753d9509b3cb0fb2eb0e29b6d6c79b8471ba4dc7dd5b81b13d15b +size 50892 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png new file mode 100644 index 0000000000..35ba5bc8ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44fd4c046c89628b32d85c7732ef2ed6a50b3e02928bd2409b6bfbb0c6aa3fd9 +size 59064 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png index ef8699b324..9685e6cbc9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f21b1799dec6cb466378a748adb5227f76bd6cf611be694922ef63a951fdf692 -size 39910 +oid sha256:d1a28720d554bc4698eb52e27e7c6228ac9c14b3703a8c139c3113f8b8fa284e +size 40355 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png index c19f70ee0a..db61d90955 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc8b338c1a56b1b663171e16815b3c8122dab3ec948f3db4b9e790833b3f89a6 -size 55378 +oid sha256:8093bd975c14cf5da50203a039e0c292dadc68183fdee5076adc217058b555f8 +size 57236 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png index 3cd7dd274c..c891dbf206 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2a89e79995050e8b24b459172a7c123cc08c64097d5d735cef8c47f913a3bf4 -size 54080 +oid sha256:507ab916afdc3f4d1429ef70c6e19dac8a80713636b064657430c19252494098 +size 55558 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png index 767be1a188..42c31db4f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:970396f75edf55841822286155e73e5f4981c347c7c2ff34e69ff9e4bf0aac9f -size 59343 +oid sha256:50ef8852ecea57a766f5b074ab4703d753de424c403c9fa778614d7b863e0e98 +size 60393 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png index b8d8a2cf4c..f654a4403c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cb21bd5e7d348d1d07ca4b6a360f26a4735cc9760fdd7e3b4b1bcde32da6f08 -size 62655 +oid sha256:e11d2920b56154389cffd1d56b986de2a570bff474d4f4861c710239d27c44e0 +size 58442 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png index 157e74f7f0..b8d8a2cf4c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc02a7c7b38753d9509b3cb0fb2eb0e29b6d6c79b8471ba4dc7dd5b81b13d15b -size 50892 +oid sha256:1cb21bd5e7d348d1d07ca4b6a360f26a4735cc9760fdd7e3b4b1bcde32da6f08 +size 62655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png index b3c490ad32..1f5ddc83fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30e13e1b91d681088ceba71f21f668a5d8b8f376cb10832f0289b1b14c1103e4 -size 55569 +oid sha256:3f3b1665b2038873c849362b16a04713ef89d17a9b3e44dc828754110e192e33 +size 56407 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png index d2b5044048..74826e1e9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52441a5e250b027ddd40ee32754a63c6206762084f47ecfa7e057e2ac77e78a8 -size 69120 +oid sha256:805245ffe7f4e99a0f5927b89bcaa6ab8eaf7cc603d352b14b8109e76eecbdf2 +size 51742 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png new file mode 100644 index 0000000000..c88cc50675 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:674d24421c80412d52bc3fe4559d0518a37ae2cee8e8a1408e2085a809816248 +size 59731 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png index 51a300b462..d7eb027558 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84a6b1ef879f1a9be16526efdd546aa554e03ea74b0e8ec03178b2a860b752e5 -size 38089 +oid sha256:5789d10db607ad77acd5d9391b970b2e0a0f962c205b3bf40db5c78d0dcea48b +size 38821 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png index 250279e2f8..063684db1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f9e76f5d50ce3e0fa259f0692184564a8a988c2e0ba047595c34a99c04fb8e2 -size 50510 +oid sha256:7a23be24d65190802b26b4e586c80a5cadc6451ea7d351e19c777947daa688d4 +size 52352 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png index 6b4cb7360c..b2e5208390 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abf3863e6d5ddc747dfea06a006983345937cd85d0e89b60bc625f4ad16b2691 -size 52906 +oid sha256:72460dfd88da9b6ebf586e98a819dcc6d377da3515df35a0a2b7247fa376f7d8 +size 54987 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png index 5e28ba8938..cc87882600 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5c6ac53f33fb4338a11358a487c2042304691ecf50f61ca0e7cbb75bc227efc -size 58294 +oid sha256:8538de17063819eb6aa0a614a8f108b122b0597769f253b538775d2b90473276 +size 59186 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png index 6e371532de..46c4466773 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86273e812cbc6245527c3d1f138111ece99375aec0d68728882b656b01687bff -size 64392 +oid sha256:c7ba8b81a3df9e797ca317a450a7f1bf0a8391aa6b919136827b5ab8e324cdb7 +size 57057 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png index 74826e1e9e..6e371532de 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:805245ffe7f4e99a0f5927b89bcaa6ab8eaf7cc603d352b14b8109e76eecbdf2 -size 51742 +oid sha256:86273e812cbc6245527c3d1f138111ece99375aec0d68728882b656b01687bff +size 64392 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png index 175a0f2bd1..cf8d2098fc 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c864988e49824c3618904cf42363c1fe538800b1f84aad2c66ac654f62f434a6 -size 21829 +oid sha256:d9ca948d2f322152fb3d744352c516da3969464c58d200a9a86ca4e94a421782 +size 21779 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png index 214d636e46..46eebf12ea 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:452d8bb3e6e0310e4c73144aad5a576786dc48e4070aaf63bb0c3e9da0159595 -size 21040 +oid sha256:c3659fdb2296ba5209f1dfb9715f5aa15b53e40950ae2ee874668382d48721bf +size 20998 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png index ab26a4d6e3..30bab8d633 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fdf37958fbaa5eb87b1ac06664e28d3aad4dcf49b2f778216caf469e00a6208 -size 21558 +oid sha256:c840fd342d8e0d20b467459ce55dd6efe5e8af7be63b5f04381f8b207bf0ea20 +size 21506 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png index 7f82d27887..dc32bb8c39 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:449174be2973e6ead2b3ead2e0e17602c81d1e033d4748ca73c9d23fb85f786e -size 20691 +oid sha256:cc466128c5ddf240505abbacbca4b64c4b5386c1f11c4b2490d7489173aaeade +size 20652 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png index c9f8004adf..a6c63537ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b905fa0c4db60d00f0b6ace10475938e7475f9a42485a196106ad775b0dce94 -size 21637 +oid sha256:b1280c50f1cef592973121e772c05767b3ad7dd73a69fdef767be3ff7ec0ca32 +size 21581 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png index ae6a95dc6a..51329440f8 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75e459f015375792833cba105472ef353246726cbfe9558897b439a8f113d51f -size 20793 +oid sha256:3a04a75d043d911aeb5378ee65ae711edd6d02366ea0965e3a6db4cad30b1327 +size 20747 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png index 8830de49c7..c0026065b8 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12c33a0e0e3c2adc8c84d2abc6c454481ec5525bfe4335edb76f29b8e0c47530 -size 21886 +oid sha256:1e7d403a1598ffb8f4741e8c1d22ef5ecad8a00d341804a13408bbba15f20102 +size 21852 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png index ea0c67b60e..0ae568111b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f0195e6a8ac94d80fc8fedabec7890c1759a9c8f0869c04f4091d04d6d458f0 -size 20909 +oid sha256:7cde74ada77b24ba095076aa7bb64a3c2f176a5d918366e047869fe8a014ec06 +size 20889 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png index 30fac7b3f4..c12ce989e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:feb7baa6462360a9867bf933ecd58e8a93903319d8c481f08d5f5eff68327809 -size 21767 +oid sha256:224efbc64460928f67f36aafc136ed39c08af1821681f174575b439cf3f838fc +size 21732 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png index c04d74a07a..ab4515dc83 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:794736fcffb4b5b5738ec8572b7e05aa74fe02571a187d85a3cec8ded3385944 -size 20804 +oid sha256:327382d2c29eae52cabfd23c8cbaf918b476bbb4599b34c895ae2d8cedc97ae8 +size 20779 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png index f44e0b112b..c8d2b45932 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ed073c0daa096dd650b271bc5956937147b03ed368c3ebe2bd6bef65a8ceb55 -size 19940 +oid sha256:8b006278f82e5532708959622bf2ea02a36a0958fd88fb17cf01e578ce6d041c +size 19886 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png index 74018e58c7..53cae5715b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e5ed9ed51f96eaf89818c5bc8eda71ad922b302b66167e325e75167fbf7830c -size 19250 +oid sha256:a24f72385732af0d963bd4d4ba43f4b19e4cf884622ce492adc8773724698564 +size 19203 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png index ac3c021439..1db5159272 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf3dd646cdb9edbb57f67004b27293be4c207468c951d21dcd38736d472c4572 -size 19671 +oid sha256:7871b48398cf6fb5b7879bd7fc6abdbc77610175bc1f65694263882074825b26 +size 19607 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png index ffaa88b0c1..51821ae3bb 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fba79bbabd808008241427a7fb9b679e14cf421aba27cf07f858a22e8cde986 -size 18907 +oid sha256:3a454b9e5738339b8f41b777097a3a97cfdfddfde82abfe2f684baab7637fcb9 +size 18863 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png index d0bf4cbe70..08887dc916 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:535f0ebc9954c43d6bbf5aef6b0a7e3f601f6b144ced1ab8849026fcd9b1d24e -size 49054 +oid sha256:06bcc1250d5bd298072c9433f9bce37cd732fb978c3ff8cdcf5a19533a2af75a +size 49012 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png index 3ed6965647..16468c9693 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d7d8b78fa215494248859996d72763077d915be680ed07068195d9cf46aaab9 -size 47785 +oid sha256:029032fb1ab1f7b4a6f18d91093fbdb2c1c84b8ed60c1f66827028e538f2a04a +size 47729 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png index 63a0f51e47..337e63ea5b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e6432c3aa20c0d2ee9e070e3b2c076aaaae281af0bf222d26f762a91b5051ac -size 47262 +oid sha256:b56a9d1d8e97f181c5e28644592246163aef7e8d340bb9bd40c710638f3fe15f +size 47199 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png index 2f538779d2..4aa852f6c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc90d8a47ebb2be4d2191735dc8b672e350f65f514feba064442ee26712a7e2 -size 45634 +oid sha256:af665a28986ef9134cd09636deee127b26118fbb53d2a17aeac61b99949c8b49 +size 45575 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png index a73ab70bbe..7072bbc0fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:737a2a37bd4b753981d719e70c366688a92ebfca62e53a1e60ae81a073c40f14 -size 48858 +oid sha256:b3e60938f89fe4e56acec088683232dcce2b13deaf55111ef3e049857dfe2585 +size 48824 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png index 1f3b56bcd9..1909c9455b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewCreator_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffda5057a816628a946e6441a90c47a469233a61d3c8da11d39797b8f44adfff -size 47649 +oid sha256:2aef02a3fbb01e4dc03179914298aa169800e2f866d678ea8fa53a492e5e451d +size 47577 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png index 804455160e..276c82c9ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ebc7039d4c3db7d0a7cf36b7cb317576b9eebac16bd29fc542193ddc78a3467 -size 46843 +oid sha256:35741e180e6c07b452c485099c11473e80ac308ae81d3620fcd8e3bd1385f977 +size 46801 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png index cdf797ba54..85edbd5662 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:effa17c9dc842c49c1b67be244f0c8c53e859fe80dab54a373d7536ce3898e7e -size 45498 +oid sha256:9b64a00c01448b8935985343ae33912c80dd84ea2329717b8addfc07dd6ef012 +size 45403 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png index 63a0f51e47..337e63ea5b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e6432c3aa20c0d2ee9e070e3b2c076aaaae281af0bf222d26f762a91b5051ac -size 47262 +oid sha256:b56a9d1d8e97f181c5e28644592246163aef7e8d340bb9bd40c710638f3fe15f +size 47199 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png index 2f538779d2..4aa852f6c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewEnded_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc90d8a47ebb2be4d2191735dc8b672e350f65f514feba064442ee26712a7e2 -size 45634 +oid sha256:af665a28986ef9134cd09636deee127b26118fbb53d2a17aeac61b99949c8b49 +size 45575 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png index 0fd343dd00..66c2cba60c 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4153f27072a59e50417c450d9c6c9a48022f58aebd4790ae4b7721fafeb06789 -size 44788 +oid sha256:79194c49b72f05396e3ecc9faf9041edaa2fd98bb0a154cb1baeaf3a95701d0d +size 44715 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png index bce5d3b342..f59f7fb149 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55a9bdd99147171bacf97a0aa1e9e4ef04cf09ff07d6785c3fed7b09c24acdbf -size 43271 +oid sha256:bb8b80c458e9d6581e0a33651dcfa29ae0a0ab610913fa686a5719e4b887f7df +size 43196 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png index c3fcb2f17b..77f3d11e97 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:038ef6a808291c6c03fc9842333717a72d918b41dcefc30c3aa7e0e5690d6055 -size 31824 +oid sha256:72d279876d8b58aadf1e8aca76f272b5a785f93fe3e36bea98d7aabb9334bca4 +size 31867 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png index e445e1c782..f9c2514881 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb823650a3a78f45953cb968a30cc673ee0e5bfee8b646212b652f2b6f3ae48d -size 36228 +oid sha256:d49dec2d63677550189700908d870d8b3ade414dfd19ce553fbdb2847e604351 +size 36199 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png index c08d1bdb2c..54934bcf31 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:280db5d8836456fe0a1e0e080a1274dab4a42b735608a28198e3a40f59ef15c9 -size 37918 +oid sha256:f77908c9549e885df89311d21f67cb851739cb62b88151855b3ce544d91341ac +size 37990 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png index bb091e461e..20409b5c94 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6a1b401b0420d859936e6bfb65ecc42b8d3eaa600fddef9be911c756d78fbfc -size 43897 +oid sha256:0ab5170975b38b931f1ae28e842e1005535027a2d5ccaa3eea98ed92e7557322 +size 43922 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png index 4dabd8c886..f98c4227fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ec17842ff19c481f860026ff97dfe46f0fb48b9ff85c2f95a03ddde8fec0b24 -size 32486 +oid sha256:855a852546c6a2f33c079b585016ec71dd375b5b74782d0572388e47ef6f3d8a +size 32507 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png index ecbae20f54..135ab09593 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:007187362281fc7abc71f94ee8996f966d55a710f61988adb09ac73f988e3922 -size 33734 +oid sha256:c3d58d95320f94c75586d575f94344fa118298eaf157d9c2b795f823ea582457 +size 33830 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png index fddc69fad2..2e466a7c4b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dd7f6bb021bce1532ef665c518fb12f45223e8fca3f90bf30d05c2d6d03d709 -size 30830 +oid sha256:8d0de0ff521be78f2117831acf64bf0036c130b4caead8017419425a9b627dd3 +size 30911 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png index 882d4fedb0..bf5a258bad 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01f7425cd0207c270e5b4d538b89d6258d71a8b60dc40da3438233bbe6fa1f75 -size 35092 +oid sha256:1091ef4ff6a828ce7b493c9f824012cfa23bf7729d6db8b2298cbe65aa561b84 +size 35016 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png index eadb4475c1..8f9b15e40a 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ae04292e7b83d8347c0f15e3c37cfa5803568474f03d8fff87c67fff272346a -size 35835 +oid sha256:9d86c455cfa28e948b91f2fab42333efb8ee631de9a96dab3f8d18a6b2952f59 +size 35876 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png index 1fce182221..23ccf42ae5 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:472d4808b40eb045946656cc6a32470e90afb3208ea6233ae4d0c99b464be520 -size 42596 +oid sha256:d2414a68d170ad6f2267a9d3a760571059c7623c00947c02a05c6904b3536af3 +size 42601 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png index a386a003ce..dd4ec6624d 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5684d574033abdd23b7831dd8c8006e012f392b2c3dfb786519362f26a194a5 -size 31460 +oid sha256:9d2ba5293880bc3c40d7a3b346424cc5f514cf970213e626e1c6edd4e23575ab +size 31464 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png index 9c0a1b0bcd..c13f5e0f2b 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.create_CreatePollView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6acd80c3b53ad3b077fa0075a3702e9778430ec51966ed3c50039acadbf97a4d -size 31465 +oid sha256:5290184e660f4d1e4f22a11e27a32ca1fb67f2c3dcea1a530ad70c767d716f43 +size 31560 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png index a3c90658fb..65585d6b92 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:505d223578e9311861dbf7a20e1becb36148b9180a687feea7232ec717792f86 -size 56650 +oid sha256:b47d1d3babb5c9b59e975ececb12829df9f2e576ef1487daba8905360260bf6e +size 56521 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png index 8c8397bf39..b92844eb05 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ef271dffed2d6e32bc61d4c48d16e339f1acaea5862ee25c602c06f1b9bc956 -size 60668 +oid sha256:5faffc0ee9d5b5f3780c4eba1464d83108e65bc175e4659770ef0266d0883285 +size 60548 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png index af1499cc0b..281ef07c0e 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:184281a9d2f2a14431d4e6b50a9e876a75b75a202c8e7f598d0f60ad5218008d -size 54967 +oid sha256:1e9e1fe808bb06c2864a9ec68879ec7940cc049761e23947d3691f339deac6fa +size 54874 diff --git a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png index a34af749c5..5aa99c7164 100644 --- a/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.poll.impl.history_PollHistoryView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74ab54160b7f732840b0ddf950a6efb0708d9227a72b0c03e798c83f828cbd0e -size 58794 +oid sha256:5daf469c7f8615c9b3167246027e6f2e366f4bb9a984d5603559fd00467274f1 +size 58706 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en.png index da80c7af53..b86c69a702 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:645a7fb89953ff6e72119a105ac966cabaf8ca6a68f3023534301e3471b942f2 -size 49262 +oid sha256:fb6e775ab09eb127cd56db6831f8bb125d3ce8b2403bd986487e9ee2c628fe03 +size 59750 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en.png index 6aa030a83a..efb0ba0a05 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea52fd146c53b690c1273b090c6b66701a8f40d80eb1dc693898c3dbf8b24def -size 49115 +oid sha256:2cde83c54e65dac37f42d162758d0bb075eecfdaae5508b7790eb2aacc36f4a2 +size 59615 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en.png index a31686aaad..6d10e6b029 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf3e4ffa06a77fbaa9dc9985274d6b1324fb023f5eeb2b9d229af0854b41e236 -size 49095 +oid sha256:56e19e11105421fee374e36f824761ddff1e160f3c6f505d7c66241a81430612 +size 59600 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en.png index 421367fee7..2b3acbef75 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f68a09667dc19c935decb5b640bed2410dd16ae88c0d0ab6ca410225e48a87da -size 49111 +oid sha256:27d1cff6cdb23657a1d65855589efbd4a1640370f081c2a492f28fcb624f8bf9 +size 59611 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en.png index fbcd0538aa..87e3f5d778 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5defae968cccb0e78ceff23912a364cf72f9ed61ea6ae3cc9770af9c39b05f9d -size 49035 +oid sha256:c383308325ff9b683a9b001ace0601c04e3ae71b2bbbf3e07aae1917e7bf6e15 +size 59498 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en.png index 7a3e1e9682..f4c1154539 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c590f67875f9bdc460ab82b59cd3c9e11147bfadc59028552071b64fd440f211 -size 49259 +oid sha256:7b634c94a7c36c771c7c7cf7b42f05151aa03479448e2c10ae22bf7e058eb28c +size 59746 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en.png index 9c788eb3ab..738db2b489 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d48af05c854a8f313f10aaa774e4268756dcf7e0c2bea0e138dc6cd84137673 -size 48995 +oid sha256:4e27c596d0e2462b5a469d27bcfe9073c09cfe51a91cf280599919ef25b74395 +size 59292 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en.png index 9d375b0b74..5092ac3e94 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1507eeffcfacfcf6c84494f09d89ef3478748ae31f8fee4a9922e4188ccb454 -size 48563 +oid sha256:596e9308895c9a8fec14ed26af4ef348f8c3f5772eb7f296b437579c59dabda6 +size 58804 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en.png index fac0ba3da5..aa72afe58b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewBlack_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4146fd85887aca1a0a65ef559976c1436d1fc2b0de522295f27bfaef1d904ea4 -size 55410 +oid sha256:644204d979bb19ec5a002ae246814098af05e8750316d52910f649ff62eb8b6f +size 62866 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en.png index 9feee4d98d..473f98a32f 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b832525da2a7744eff8165c842652bfff4742b728b000b1c8ef8be81bca75efe -size 47052 +oid sha256:a80835ea11094a66e09528413ef411853ad8218f06051cbd77d5b32de32e8e10 +size 56895 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en.png index 9a4a40c0e9..58f2639ace 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ae8264008b6396f31332263472c910b492fffdb9dbf7d0186b44a272476c02c -size 46903 +oid sha256:cf87b258d11b13e98e77b145da51afa40d56248682ea2ef23fca320e190feafd +size 56746 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en.png index fa74fd4b96..62134288dd 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4842f6e88df23ae0050f0052ebe9581d6c19ac5e29a0c553716012713fbb00aa -size 46901 +oid sha256:2f5cb1d3f9effd4099e1a27efef8e3bd40e911e6e935355944090f5ad62bdfc1 +size 56745 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en.png index 5f9af21b6e..c83202730e 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7888dbb0ed5460c54796a6b2c232945bd4449e57feda4aed89ddce097a3c35b -size 46911 +oid sha256:7f862f27162968dae996e23baeec3291645b125ef25f853d9b9a363d24aca738 +size 56756 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en.png index fe34c6d5c1..77452af18b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:215745acc9dc0c86e239acf614c94f1cd46e96cf1b6dd7cdca514d7a4a82f835 -size 46742 +oid sha256:98a269501ae382129654d04852123377be4e6c0ee1b665c989dcef3a1d44a44b +size 56623 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en.png index 9df6b272e4..c0806414b6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5d099940f91cb3e520f6eb6cbbc075bc57817f3e46e3f9fb169a36b408388f1 -size 47046 +oid sha256:e2342cf3ccf4c59f1bd17d4276c5fd7f29b41cc1a17105272cc1913287e435fa +size 56902 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en.png index f56d4e2115..c79abdd7c6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce9c49a9d136d3bae5fb366fc47d3f48f8fabbe6963d0e53166dc1a5ec4cbc20 -size 46695 +oid sha256:3b14f3c517b570a0724f6ec14ecf3c0ba8881fcdaf643d6ce9070f0871e03fc7 +size 56464 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en.png index f19645feef..dd8afdfe37 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed86c0b3137dcd650d7ba65d7ec009146234b4e0b983b1ab8b56974000698201 -size 46242 +oid sha256:46aca63876af82eb27f9b7f050716429ae323ed626793968f620dc2464009db6 +size 56146 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en.png index 2b725b9e8f..e851a8517a 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbdf230e73c3fd93787030d6f5eac65b6a8131bf2eccb07fcd8474f191b87b06 -size 52563 +oid sha256:0e73e9eab3864c65badd7660306cfe96e0b45d4963c7cc1e961d330da074b24b +size 59700 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en.png index f9a16c247a..edb79ba518 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d57215dd58ace85ad235fb7f95ef592bfddd3c21afbf183bb5d31fbf772b69a -size 48900 +oid sha256:30a586c9cefe4f7d9b342b792d65314657e3ca7f56a785d7779d855fcbe8a428 +size 59233 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en.png index e662b2dfbe..c747c2e1a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32926b81da50838d883edfd157e7d12cc3d43991556f87e61702b74e693ab6da -size 48798 +oid sha256:3c15192a3094d3b86613c37b8a40391cb6ade53937c172a27d63d2dd644bfba0 +size 59134 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en.png index 5ac95223f9..440b244d12 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0594480e59710892e480a83c1352a501504ae1bafb4c5c6b672c7dc62b3e75e1 -size 48801 +oid sha256:dc6cbdf0c90d70ff85ed3f5133eb1f938602c583639139b9055d43f4b52d6c86 +size 59142 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en.png index ca2e552c10..868f9d3682 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba828796a14ee9e82e01e98a89067649a7f3438a2fcc8e3ce5f69e12e86f86b0 -size 48777 +oid sha256:b9a5f7b9ecffd994a28a21fabb07f089f4bcda19db05ee23250e8b88966752f4 +size 59103 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en.png index 6f741da48f..0de67666fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ebb88fa29993c785e6ca82543e7eeafefe365c0018a4f22852b988dc35db4a5 -size 48716 +oid sha256:2ef7a874894de12c6d6e39fbac20064fc35bc3b56ad17c42e2bf06412f2f90e1 +size 59062 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en.png index 1a12af9468..38057861c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2521e5352ee6030f7a0fd8304b19f6a7a95fb6364d43f412eeef03799c8c0f76 -size 48901 +oid sha256:e216b29be5c59b8b32173a478e90f9e9b965b4eb7703882d2b8b4499eef4d1b1 +size 59231 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en.png index 962991ff8f..0c76f03f80 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dfcf074e7fa4813218f4bf9e472f706c5581e6fc5c1f7ea1eb9ec422172190f -size 48713 +oid sha256:d9130cd7bdf755cbb244cb77cdbb741d4fd97edd18a4d2b342a4d37b9155dcb1 +size 58859 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en.png index 8917220310..9e39d93bf2 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3195a1871862ce32cdcf068608a565c56e794ff66f14473d5f6355918db79b6 -size 48402 +oid sha256:d96caac765e3945e6a57c0aa136ee31c581406f90f397afbae2b79d0449873ed +size 58548 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en.png index 5fc76943b5..a4f99faf45 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsViewLight_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f71219202890afcd4f43b5f57cb4d555b723df48f71848b64c8db415067d1d4b -size 55044 +oid sha256:d3d41fd6c09dde0db3e37d2a28442b2f7eddfe46bdc0d119773e37a55c46d0d5 +size 62224 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png index 074820e157..7cca911c45 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7caacb336793b8ae4b83e9a10a82325e25e8b3c036f881898ed28688250eb6e0 -size 55744 +oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309 +size 52770 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png index 2af459c158..7cca911c45 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28a95d4b7ebca92c4e0467f3ecd9acdaf7f6257604d190f5d088a9a25e87d637 -size 55305 +oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309 +size 52770 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png index 36d19c6b72..616c06bf46 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7dfa2767c55a81d3f121b8fa55443b7b05f9369e91639b80e0be349863799ad7 -size 55438 +oid sha256:0ce2519906f1aaf34700ca9ba4bdeaa72030f1f5aff3f7e24352a1435ac67d98 +size 53966 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 index 708313c475..9e5e42f653 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62068492969ad00e1a8e4a44189f93d83c98fee40612e4eecb68d3076d00ed07 -size 56425 +oid sha256:a955265e37fe0a06065f70b0b9648c8cb85b5e6721336deb7735721dc3c0db80 +size 56623 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 index b69265cf41..3155968037 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d92e220d675097c37b60312ca4c4e821eb6ff6475bcd004437dde0d2e964cce -size 54756 +oid sha256:ddb64ac02738159cd0897e7fdf62d8138ce1590afeb05ed7492a41fb361b7dbe +size 54681 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 index b56ed34048..a33ca80765 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf121c0ea1fb3cc7a47a292877535c9c0a1ae1ac11bf7f5ce169d0cd79844246 -size 53775 +oid sha256:d7de600ae1a687929c01ff52255571daa496fbd1c7df8a94e56ebf00f0ef8392 +size 53595 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 index d0a5500209..a45f19a0cb 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37ae25b9f9c659164c006c9bdb9f94379398ee8215e8a2dd9167620ef994b6d1 -size 52347 +oid sha256:d9c0458f9bf81c02fb10d8d9faf88f3c0813813fc5bad7d3ceedc7cdfe3028e3 +size 52103 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 index d2929c43be..81a02b4ea8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db439869d2b8843cd58251bbca638e0dad7ed2b1ae4b22507e052e3d5521214d -size 52090 +oid sha256:2ba238914f025ca392c7f90d6800f57adef47d48bab03bbf4d711cc2f6036c32 +size 52222 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 index 540b910b54..0b7f81266a 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cae56bb14ae7bc1f6c73884c2efb333b7da22c82d69026854a59c380657a5bbe -size 50705 +oid sha256:ea5d9314cb0466aa16c7d7d28056ccf13a60a493a88104be2686384e66722b4a +size 50727 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 503b9ad61c..48914fa409 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:d18a2e9ba19401f958483bf06ecd7b311ed7383de0df3d89f3bb4d3a57f8e9e7 -size 54191 +oid sha256:59b02b82b889878dc908a859bf32c1486fd926034b89a258028835aebe9ca873 +size 53872 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 16be314a0d..dbafdad6eb 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:aafbc1f791f067fd0084db3bf293511e3cf7557329e7599c3f3e3c66f01435c4 -size 45472 +oid sha256:a6c6e4d5ff9f34adbf6dc303e107336d675c874ebfc99a903c44b37693c521ac +size 45444 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 503b9ad61c..48914fa409 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:d18a2e9ba19401f958483bf06ecd7b311ed7383de0df3d89f3bb4d3a57f8e9e7 -size 54191 +oid sha256:59b02b82b889878dc908a859bf32c1486fd926034b89a258028835aebe9ca873 +size 53872 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 430756b5b3..ef953a39d2 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:dcca87aa6eee7e45bedb8f58c3b776e3932d94843573eea70aa686c3de82e03f -size 52290 +oid sha256:51947f9062518b5ab89b9bdc20889b5560146b182a4549f3a2b6f7a5d9ca2ee5 +size 52298 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 4225562f66..2ad5c53567 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:25e311c9bd46defd9659004d2d36088985c0578189a41906b51bfe683ddb6488 -size 43889 +oid sha256:762fbce69b40b9b8c1209415a88fe95f0480ef87e1dd2c1748bd7ac3b4cc1d76 +size 43835 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 430756b5b3..ef953a39d2 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:dcca87aa6eee7e45bedb8f58c3b776e3932d94843573eea70aa686c3de82e03f -size 52290 +oid sha256:51947f9062518b5ab89b9bdc20889b5560146b182a4549f3a2b6f7a5d9ca2ee5 +size 52298 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png index 7bf37a76f8..cbda488267 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5171c9f08d2249562d83d9967deb84cbb59bfe9613df27097ab342890405ad6 -size 42377 +oid sha256:731d4139bd4b0f2876e5df8a481b34e4ed4678f518b81c1e101aca07fab6e54a +size 42155 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png index 2ba28bb753..63eeb0da9c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7b61c67d3fe6366874f7a0048c6e82c66f607fef586fcf0b43b48b1724fc602 -size 42230 +oid sha256:8581ef2719ba34504891e9a5ef46b7cd06d3daca5894b8d664eb067d55a76355 +size 42004 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png index f49c317324..4fd802402b 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9654dec6e7d312d9e1d0189e928ec01de8aa86c7d9b8c8a3ebc5ebadfd3588ef -size 35493 +oid sha256:1a3402b8a8458654e8c1c1a7efa81af45f8038b85bea027bcd9b2476812ef385 +size 35520 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png index 80c7478e20..9dfb896c03 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae1fc7261b3ea8adf68f2c9a7e88e91a77a816713ac5ec20dd65a32670d2d3d4 -size 37859 +oid sha256:76b6c58712a347586493d9857aa5de12da23bb2603f925f72a2b59f4a3ba7196 +size 37916 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png index 89a6d30f0b..6f434d15ee 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27c60b1d1923b1546215255cf9dfb78091802b40abf7f831375a5e0a9d24cd25 -size 55685 +oid sha256:ae04c1ccea9073ab548d08995c427b904318bc3aefebd278622d71b2ff89f8ad +size 55578 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png index 90e3292e27..ac43ffb23c 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:156307f3934d6bf4fbc90b36bd6e1e7e516e1401b7253624af93de9801e4fc41 -size 41920 +oid sha256:08baa541e27b1c029e18bdb843c1915997f7234e496c2c8c7583d8a888044f05 +size 41768 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png index a595bbe51f..00ca9c12de 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b84aa300ecbcbc9fba740dd3dfbb90fffbc6fa463e40d009ddd5ae394b50c5ea -size 41793 +oid sha256:426db78c889d357ed94977990d2d4ab8194bb48ec4d0466cd54baa8ed1676809 +size 41654 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png index 2aa655fde2..b5076580c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac2d31aaac66c11e62d45074253efd95190804b5294d7837781912d61cf98a64 -size 34715 +oid sha256:5cfdfb972fcd32e87c8357567875aef7f26597e09a5aabe80173b4ee758cf92a +size 34760 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png index 89459812cf..6cc3a12c88 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05896ad1d86b38825fe1793cf83db364404d998585fa60b456061be2093d848e -size 35660 +oid sha256:35e801a6bbfdf73c93a3887e8097685144b33ed7bad142901e878e474ac0f437 +size 35692 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png index 9957d2032c..fbf89da91f 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adc28c6db29c01547a811d825b1c24b1db94a900631e74925edba372760aebd1 -size 54705 +oid sha256:8674e93ab6223d1bde21df6688482df5adc16a345339d593c363018b126cbc26 +size 54706 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png index d927ea14b0..08ae236377 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01999b61120bb58f8cfa56a2f5b1b5573d2755fc5be85d3929237d6a61244ab0 -size 54308 +oid sha256:c0aacf68449e737c3c357e974cb8756c65e60db7578885ff9ce4bb726d31bc00 +size 52008 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png index 41290c5db8..22d0e1eef7 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74890836fd7256b068ac02e78c83b20a3b1d22fe9e5d1bf6a939f7d6f03abe78 -size 58686 +oid sha256:e41f07dc61af91ef995d2b04cdaf9d78e733189d03b04a4992184e8e1134364d +size 58640 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png new file mode 100644 index 0000000000..ee21ee384a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47ec77ef02122184cab59688e5ba3f9aadf8875fff73175402c3da499f591ba1 +size 43317 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png new file mode 100644 index 0000000000..8fcb545029 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_15_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c9e7fc37451a08d7f0e8b1ad2d0dcd75f428114fc8985dc430dbe4b1c913309 +size 41879 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png new file mode 100644 index 0000000000..41a60a992a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c14ee768960796a0d31d6f7de0590a3cde41918eff0aa7e9158392a4a4fce703 +size 43173 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png new file mode 100644 index 0000000000..a421925d73 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 +size 43046 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png new file mode 100644 index 0000000000..4408ec767f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_18_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb8f82d825a0e97703793b37c8c9525271aebd9cf33d3a6de1422e8515f7f97b +size 44930 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png new file mode 100644 index 0000000000..5b4ddf6763 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_19_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14b9e7f22078be909317cdb8b2823919a9d0f6a179f2964fc855252b4237ab09 +size 39607 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png index 4302a4c031..a421925d73 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52f7675544ba71ed73d2f4283287f46eaa0798d48993d7eb54fc28f87c89cf0e -size 43466 +oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 +size 43046 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png new file mode 100644 index 0000000000..0f7dbeb57c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_20_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c048e3b635b98cbd4ce8f04278f568888413d4a7e2414662f57ea6ef662c2a22 +size 41665 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png index edcd62c95f..bbf9e79392 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50cca191bf1c21a4d0f2765643712d0db6813dc23f2c4b70589ad65f2847b259 -size 40215 +oid sha256:d1803afde307769d808a1c4f9f764da822ee5a774d3c159524b66852942157c6 +size 39980 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png index caf2a0b795..71d8627267 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c13b82c858490228a4f5568b7489115d916af067a8cf2e01e22e44f56cd6c3e -size 39566 +oid sha256:f53a28516bfb1898bdde08ea2c8a408b0e4533e0df293ca526460366eac2ed30 +size 39271 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png index caf2a0b795..71d8627267 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c13b82c858490228a4f5568b7489115d916af067a8cf2e01e22e44f56cd6c3e -size 39566 +oid sha256:f53a28516bfb1898bdde08ea2c8a408b0e4533e0df293ca526460366eac2ed30 +size 39271 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png index 4302a4c031..a421925d73 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52f7675544ba71ed73d2f4283287f46eaa0798d48993d7eb54fc28f87c89cf0e -size 43466 +oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 +size 43046 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png index 5b314cc55a..ff8c8bfeeb 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38839f3e1cc1ed7fd02695e1ad68e44692fc836ee3fad075ada9d500a94b2554 -size 39554 +oid sha256:964005811374c5747c378220205979cb646df301755176d270bd3f8f6a3e5b91 +size 39341 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png index b292bd42f5..0f9cd69b16 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc4c290503b6ae3157cf4169b50a8e00bae953d87b2d319389e8ab3decb9ca3 -size 43856 +oid sha256:d0a6d0e6c1e54a1842178576a2f63b29277a116c3d63e1e83c58ee2a2061c43b +size 43680 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png index 4302a4c031..a421925d73 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52f7675544ba71ed73d2f4283287f46eaa0798d48993d7eb54fc28f87c89cf0e -size 43466 +oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 +size 43046 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png index 4302a4c031..a421925d73 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52f7675544ba71ed73d2f4283287f46eaa0798d48993d7eb54fc28f87c89cf0e -size 43466 +oid sha256:44f18c872ab94dba349b9b58d285aefa3e32e79676f37f63ed9c50fe1de68e49 +size 43046 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png index 5c281630e3..e9066fcbff 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8a677a3dbb1cf6cbb306d3b1e4ba612469cba408815320361365d74e188cf74 -size 52703 +oid sha256:7019689badf7430a8a3a12eeaafb575de4b08fc2872a383d694085b4ae41f6cf +size 50545 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png index 6beab389f6..f8026d34c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:684d9d7475db5d003772337aabe0834ecc95079105d0820f387654a7db1f5a37 -size 56947 +oid sha256:57d7d8d9e6f0867662121cdde52e32ef7be19760bcf534c2f4586e0d57cba03c +size 56878 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png new file mode 100644 index 0000000000..2e127ed838 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_14_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d93a5e5632ffff786845b69b532994642d04c2d306ede1d8f246ac157a73682e +size 41882 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png new file mode 100644 index 0000000000..71d599d5d9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_15_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89456057ead8933e7ebf3c2f4210da1fb92a1c9d4fee10ceb832cc0d3fe3797e +size 40524 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png new file mode 100644 index 0000000000..f22aa4e4b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_16_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44ca73c0eb048a81f77d2dd70f785261024419ef77fa0a809ee101019db82c62 +size 41695 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png new file mode 100644 index 0000000000..56981dc8a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_17_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb +size 41684 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png new file mode 100644 index 0000000000..88e2cb485b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_18_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ead1fb3338330fb5f6f3178b2f71ed4d1fa45790938db9045b3cc8ebad85cd88 +size 42167 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png new file mode 100644 index 0000000000..a5dd69f5fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_19_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e0dc3c944415c2739e330e7cc0c284c1ccd1b4b4801c7b300aba0af0a144c3b +size 37068 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png index 05b8276ace..56981dc8a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d92a5ffd43eac457c3384cfc22b9b29fbf8c21532fc16892258140c557788bd2 -size 41872 +oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb +size 41684 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png new file mode 100644 index 0000000000..6fe937e597 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_20_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bcaf45ed5dec3e86d4b17e5a53fbadd455df81a4b30924a6309e472ddaf2636 +size 39206 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png index 56334c1e2e..242258f056 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd1599665f32844b4757a631542351303b7d4f4a34f32640ee9f60ad38089cab -size 38707 +oid sha256:afd1e21c834ecd9c3d0405749ea458672d2ac785c9f4d615ba2d780879d11a42 +size 38489 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png index c7e3274c19..e87402fc99 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea8532bc891ce639c492ed7d6edaaadefc2b99dd3b68873a575858b03b6fcbfe -size 36998 +oid sha256:fa9d8b31efc5b2c369d501bd002f66edccb84e812fa10d5b3284ab13357f7404 +size 36831 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png index c7e3274c19..e87402fc99 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea8532bc891ce639c492ed7d6edaaadefc2b99dd3b68873a575858b03b6fcbfe -size 36998 +oid sha256:fa9d8b31efc5b2c369d501bd002f66edccb84e812fa10d5b3284ab13357f7404 +size 36831 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png index 05b8276ace..56981dc8a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d92a5ffd43eac457c3384cfc22b9b29fbf8c21532fc16892258140c557788bd2 -size 41872 +oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb +size 41684 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png index 1f3b3e2b91..fc16482e71 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68d4a4cd27afda0d23438451124ef997dadd116e1ea5db4b5a6af0045d95ba2b -size 36998 +oid sha256:3efe00ff653c1c6efdc85ef3ab244d8078f6f36cf6cde2620c4b4f5776b019f9 +size 36861 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png index bbc69da0c3..fd9b76fd36 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:784b744139daa4216d4137aaaca3a4f1a86ef7297ce2996bb9e9eb78c8aa93fb -size 41201 +oid sha256:67539cba9fe0e53fd902a878c76b0927cc0afad2a9541781e2ed4a81eba652a7 +size 41089 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png index 05b8276ace..56981dc8a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d92a5ffd43eac457c3384cfc22b9b29fbf8c21532fc16892258140c557788bd2 -size 41872 +oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb +size 41684 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png index 05b8276ace..56981dc8a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.notifications_NotificationSettingsView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d92a5ffd43eac457c3384cfc22b9b29fbf8c21532fc16892258140c557788bd2 -size 41872 +oid sha256:a4adf5e05b575aff79e8e0ae1bc039e147b014190cea303abc3dd9e1df6f33cb +size 41684 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png index c98615fb74..6e46211c3e 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1da26fc2ffe2216cda7aa1b6b92ba587c9d0bb56aa54586b3073befee96d29b6 -size 40956 +oid sha256:cff89dace11868ae57ad7c701303ab19096be52bbc21f7b43f91097fe8a612f1 +size 40300 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png index 3ad13553de..053b0cf1f6 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be4cbe72c73d76252f902e122bd62ba51ff89cf2cb20ddd61ed6615e983c65cf -size 25153 +oid sha256:92420711d5445edf625ff3125ecabbfbd4317dda6d7e29b4484a3b38d8d3193d +size 24297 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png index 9f15832109..2c9cc8de20 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a4be2c70eb477dbd58e4e6924d0141f1c8d7205807d968b3a34221c12a301c6 -size 36176 +oid sha256:f36ead42cf5f79023ca960698b25864e44a098b71d4de8a4fbff6e2cb0a86b99 +size 35546 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png index f1b39a0d35..2d0012240f 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9176ee2b5b78032639d9f51f7680d82f7d3ca916fb587d914f12d075382d65f0 -size 27077 +oid sha256:83085cec3cd4e3a8eb9c8b112d54d9aa1eaa6ca3206b5f526b6e0edc7392ce92 +size 26743 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png index e9cb1772f7..a89b3d05f5 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76caf93913c979f3327dec4331d3a5bea5027df45c5cacf55a786b8f677e3162 -size 27340 +oid sha256:6a9739c8908859f2401d118e32f4354a2132d6d08173c4e5683eaa1b8dee8cc5 +size 26897 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png index 31a44423ce..d5f5b7f6ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e457b722fe403205f1394e7347dedb3aba308e48c979ea002399425c9a130fd2 -size 20667 +oid sha256:c0d9335e3b6900a366d4770849736b7bc866bb205c9b6f004a3521d6a058916b +size 20020 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png index ebe4511bdf..d527233adc 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a0202d081e5aed85ca093239fae1c8f46d42fd6e859c818f5c0301597e5b473 -size 41957 +oid sha256:a911c0f75b2146fffb24d3bddac02ae096eac871285ef15c4d1477629a5b2d49 +size 41390 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png index 933a646f6b..77170a40ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d3a7ae9522da2911bf256273a4972020fc1677f026fbb9f17d27f549ab03ea3 -size 25978 +oid sha256:dee0eff6123dca9f9b3ba544d2db16b1e2ab982d20a5d8a0013b0e08e37afddd +size 25094 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png index 155302aafc..7be86a8757 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ae20b874dba38a89a59e418bce33e748449446e0fc913b7f3ff2e87b8200c40 -size 36568 +oid sha256:6fef2b0dd09457de0079e3c00ea6e07a81bfb7d0f500d228d38c9054b446708f +size 36271 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png index 15c12f4836..046163551a 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a4913734d0648115d5056052fa2de8a839bb5a4d2dfdaa3d8ed5f0eef2793d -size 27728 +oid sha256:9a8ef73ae13ff39f1612dde7582225a18656015c2608fa45510bef3d8a52a705 +size 27701 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png index 7e4a6475b1..1c8583ea33 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4f8571893bf9292a7d298c7419ea2e3f0363e1c2eca8c5f6aaea7d39143039c -size 27855 +oid sha256:691bdc37a68ad14bf1ac08ff242861412915c597017b7980850fb2a82070c228 +size 27646 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png index c325a4e7b5..5c6556e3b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5b500d6275bc6ead5e8644b1afb1a0a829e91d4912444e5e0d431322343855 -size 20700 +oid sha256:41c88c94e910f524567d7054d7914eeddccff2b0249e331582ea1ab130d40d6a +size 20454 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_0_en.png index c6e079dee2..03d5ad5880 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0805bac4bf9e1c5bb16b4f81b004bcc952563eef98101d8c9c6e856a414977d0 -size 11219 +oid sha256:0dfea70e781debf7a293aee2967c3da314101020698fee97d3551e25f5f378d0 +size 10655 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_1_en.png index 74bc9743d7..3a1ad156a2 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b61c5a72e8e63a1775d20717c78d8e68e46c7c22b8e4ab8c24154dc3d48d7d0e -size 11428 +oid sha256:33482ad6e5033800e911dab44d4ba5bec15cfa58749102c5da4e2c57a8e760e8 +size 9783 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_0_en.png index f21d99fffc..27f51b2778 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070163694fe10b465f2903ce2cc88f9ffc67d9489aa5a4e204cd087fd02b8642 -size 11281 +oid sha256:62e127900d00756ecc806eb9d8efb6a11cd9cdcecae6b4bab643b7306a06be2b +size 10435 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_1_en.png index 9413b91e88..81d4ceda60 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.user_UserPreferences_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da4e1512cd7a58ede774755b6e3ac427e90c437bbbadaea8479506af776999c3 -size 11323 +oid sha256:e39bc143f7ee45c47ff57bfb53a773d7df2ba14568a9368643d8bab9dbbf4955 +size 9675 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Day_0_en.png index 1dcb5f4f8f..3c898e8ab5 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc7a2e36694227df123681799f09bb2ee3dae24258679557193ae69ecf4c2871 -size 24012 +oid sha256:17a23d0c9d2993b81edb72f19ba6ec03f24b79a2f732e7af6d3594c96503723c +size 24978 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Night_0_en.png index 65a0322a88..b3f17cafcb 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.crash_CrashDetectionView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18bbb07caa3cae740bb80297ced37fc3e8c92b48cd0935f716a86e0664737a75 -size 22815 +oid sha256:e2d914dbf6a309cffcba36f0affecf19287ad06c751f50b23a51e9df23bfd12e +size 23859 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Day_0_en.png index 77b4b1e222..b8fb7eb092 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31a1989c17263d25d5bde65a7b0399b58c9e77d320654ac9f0e65f0fe2410b15 -size 25882 +oid sha256:eea615f75cb1c3db04cffba9798f6662376f1c1db6825aa73c2db6f10192d23d +size 26959 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Night_0_en.png index d8fdf936f2..5bc6eeb700 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.detection_RageshakeDialogContent_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28f410c8068e0d7586af4c727ce5ba131e56387472f661b34841139e1e43cd3d -size 24788 +oid sha256:56c84d3a772d03eb016807b886df9728a5fbaf07e3fc6dc2ce4353007aec405a +size 25726 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en.png index f59f8b6da0..a5c4e992dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d521236e5470339e854f490bbf4fc8e7ba355e3cc27e32145320df217be31d1 -size 18387 +oid sha256:015c7855f265ea896320f5bd3319e05a1fd09708644ed4980150e8cf4c6eec1e +size 18365 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en.png index ce1568c007..f44041159d 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:322d3e0d5d7ca5aec1814e8e78eda2abf99ee178aeaeb9c4b9341ac35c92eb38 -size 14766 +oid sha256:1c2993725d56690edb632d5a5a8776cafbaddf98e213ec5e912c9ab7637c74bf +size 14810 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en.png index bd06c2643e..2f9d7380a3 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a1588effb798e95e7b312eaeac298317851afaec7ff16a79c27ea0778ccbbac -size 17005 +oid sha256:79cf0d6e42b99ee91917aafb3d28ccb81e45042954370b239703fc3084eb5897 +size 17074 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en.png index 197a3c53f5..cc47b174cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:606ad1faa861583d9e03a05f4aa9fd315389118700c062c7fd694492d2aafc95 -size 13902 +oid sha256:ffd643d1356b6317480b9852974df74a8675107aeaa7c28fbbe62920a2e0a2b6 +size 14034 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png index b81b524b86..d600a67219 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a10513e98757e17cc3ffb8302807896839a78d036331e79bb207686f3f9db86 -size 47842 +oid sha256:112ad29fea211c36dcb15a0674f1684869318858260799ee52913514f4e3b1be +size 52976 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png index 0700c94bb7..29fc43b66c 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05efb0fa45bb6440e75e9280b219cfdac83c372efe9e6d6254b4bbe516f8b998 -size 112842 +oid sha256:0bcdd2cc0e47a7303cbcdcf34e069c556287ad30454bc36b068360d9820b88c0 +size 112924 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png index 61e347497e..cf7c538197 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:833d21633cb0f153b660112fcfd0923c588cd26b64005403a007d7782ecfe5d5 -size 45138 +oid sha256:6b4572b681595f3893188f9f78e7966d52508d4119451fd7ac86f7bb4f5cef0e +size 50635 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png index b81b524b86..d600a67219 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a10513e98757e17cc3ffb8302807896839a78d036331e79bb207686f3f9db86 -size 47842 +oid sha256:112ad29fea211c36dcb15a0674f1684869318858260799ee52913514f4e3b1be +size 52976 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png index 948604dd84..f581ee930b 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewDay_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebe22ec065cc6dff2133dcc573a82e003d9061ae0446c2d8c13f4e1fba1f3c19 -size 37056 +oid sha256:e935f2b1f46b5330ea034b6824628f8dab7417f9d185fd720297ac8a5c1ce838 +size 41507 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png index 9b379990c3..a1b520209d 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f647e92bf813ead7affcdb6ad07076fe9d739f6a4c0016b9787912f032e27476 -size 46567 +oid sha256:a6388912c7e4797b63799f2c938a02dc1680de65cbb28ab57fc1961248ccb61d +size 51398 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png index fcdfe6ba7d..cf2783581a 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdfbb9f70a97724a1b6687ad4f199efc2c9bcc5f29fe9475a17ee6e52e5ecf8e -size 110829 +oid sha256:ea54b01d2e6b607db3dacfba356ed3e3e3bf4c00ff91b3b65c748c478e776e66 +size 110685 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png index 6760c96a86..b141191872 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cfab4886243e6c914f466f5f127ec0347b0ab30ee43fb95c7bc82d2b16325d5 -size 43727 +oid sha256:c20206fd5603cf207dc8d504f43b350ecefae39710dced25f0fcca1f34f05c68 +size 48912 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png index 9b379990c3..a1b520209d 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f647e92bf813ead7affcdb6ad07076fe9d739f6a4c0016b9787912f032e27476 -size 46567 +oid sha256:a6388912c7e4797b63799f2c938a02dc1680de65cbb28ab57fc1961248ccb61d +size 51398 diff --git a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png index 6b7ad4a825..69d1edb4be 100644 --- a/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rageshake.impl.bugreport_BugReportViewNight_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c24515cc18b3c5cc52f2acfcee4faa65c02d8d5c8be847d4b1ec5671a858210 -size 35201 +oid sha256:1857e44c3a2e5a0ae49bb4bde22a86eec2e420b71e7f0aeb497b85c92c5d1ab2 +size 39675 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en.png index 90974411fc..548f4fb92a 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42115371f725ac37b484d22a7ddc0c48c5a4246360779e75256cdf2e45c4badf -size 49163 +oid sha256:d181933f188d7e5800899e6f97fababd50c3000a27aca63a2a11a5edcac8ad19 +size 49083 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en.png index 4d1dbf68f7..0150967253 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:555849e00ac48ed8b4dbaf2dd19b990d281e6db2a4787f584f5a07a02ed85ee7 -size 47831 +oid sha256:f5238d0915551c987e2e01de6268fdc4085df411103fc0a69fbc6511201e2d12 +size 47789 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en.png index 05be6036e3..ff7e8ea70b 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9b741585aec49860eadc2eb294d23f9bc275872176c45a3d0a465d3c6c85b6f -size 49081 +oid sha256:4e112e30bd75e1768a436475a63b290ac8bdad3842ce8589aa65a7a12569ec7f +size 49003 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en.png index 4887d37e61..66eb55c0d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5914d8b0f98943fd8f059853623b5f9cfec889e28699d6025feddb29c67e8b3b -size 44219 +oid sha256:9c01b62bf6bb7bd47b09660e311fee7db5fea13e09539b071ff87c29c8f0f8d8 +size 44288 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en.png index 71fafafd14..4f46f9607e 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ef05fd67b842ca7134da5a21306ff70768fc4a13676effeadf9cc86cb1ce04d -size 43230 +oid sha256:19e197ad8ea83cd381f33df27444a59cae5788b31454fe34b0c93005f6f1d41d +size 43436 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en.png index 2ebe8074ab..29964f66aa 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d49ac650aeab172c3b5fdf7939ee459773e1135f7873abbc14502f5b33d3f8c8 -size 49989 +oid sha256:a10264258982ef82c3c5692b5aebcd56d25c87b96901f497c7c0ac4ce8c2c6c8 +size 50208 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en.png index 27cf718290..df3882744a 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c4660b161de0a2daa06bcf7d931d97c1f29e8f2958b6b309df002adc180b45a -size 48598 +oid sha256:dcbe7fd975d477c3d37722cd6b95941e30ab524804b1e8a013d65fbaf0031537 +size 48571 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en.png index af4ca3b0b7..134ad1fcf2 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e351c0a3968472284aaf175fa3c36f23903727bf74621d26fc59c6979bb5314 -size 47950 +oid sha256:2d4d206671385a19afabe3ed60c4f34438c44fbfb0f950d3bbc91adad86aab66 +size 47849 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en.png index 949acd5fc5..1c04ecef27 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98f9d2867796a2031b374d1cc05f4a8218bab0a2e44ab8eee5c6feb4eafdfe2e -size 46638 +oid sha256:71ee36dc169d4f39e9015d8430aefba6785c625a5e5677a47ce71e7ed36c170e +size 46480 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en.png index d516119920..c09d6e14af 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50f74a2ef7c200d901155cc6936ace84193ebb0c801aa7ab938ccaaabe5ad8ac -size 47855 +oid sha256:f94fc5c78b20b45bea71a91d0f9ad5ee3008e0e9dd4fe7adb72d6304940bb2bc +size 47755 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en.png index 2593ef8ee3..f149763a22 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eda97e80b8302323f57d07409ea2a83ef3ba8aade3b4e329bf0bbe8176c65c10 -size 42844 +oid sha256:3019ccb270aef63a7b33fdbf18b78801ea7e55baf8d303349591d15a55d89c54 +size 42933 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en.png index e4296df2f3..e344afc5e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95a0b7d54697b6e7ee3bfaf97413991876938563008bbdf89dffe337f815afa2 -size 41202 +oid sha256:ff75cc80b45beea841511c179a1d75a4b859a3d1ea392bdbafd5d7db2dfa7865 +size 41282 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en.png index fe283b6453..63ce660ab4 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2af2d6cbd32fe906d66773c0015383bfe28035d09e43939d3ee71ba516bb1e67 -size 47790 +oid sha256:16d0a0ce6760b8ffe6d346e5ec8e962f1dd1eab6589f05762c3d0b591b7eb3f8 +size 47868 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en.png index 6dc2c3413e..6708a06368 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.permissions_ChangeRoomPermissionsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28a659ae5432a6b452476fb486486d5fb1e9bf28640f80b291a4193870106309 -size 47298 +oid sha256:1da041b4a7856026c22181a963f310e1208e6ac0337b44d639b2e35a3c0f659a +size 47271 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en.png index 031e824d00..1d74d53ef9 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b5330dd8afdb53538bccaad58a688798be1a97c2855f1766e8b5c50499f406c -size 49560 +oid sha256:468f68d04c5a4f18f2458714117c69a5d464f997f8102592017fdc670ee9c304 +size 49547 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en.png index 5405b2f535..253838d70c 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8199ec44477a9e8fcbc3507e609ca1bddcdd422e2d52429038e6289ea25066c7 -size 52137 +oid sha256:e8fb8640cc168b3337bf17f893179daf86eea29e0e9124e113ce98426a1a9ef9 +size 52090 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en.png index 596aae0702..bdbba9f408 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:826b6ed7c407abee85fff8795ca41f3cdd78936e050787b62c4da3bac91911f3 -size 53762 +oid sha256:32ccdc2e065691bd4e557eb794ee98a313011f19a5a920099612521515b65a30 +size 53712 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en.png index ec0926fe98..716872f929 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cf9484430efe87465c5aaedd53d866c7479982c77dea9ff5f5c33b659557e30 -size 58423 +oid sha256:f4acc8042fe23f5a352b5d928aee2393b4952f1cb2a00b807b35a8c0fc98c827 +size 58592 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en.png index 028a49cc90..2f262ba181 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9e6ac535c10c1cfb042419bd1f5af51b8d4a51b234ace59b6f2bece599c4451 -size 66724 +oid sha256:3e7d05b3314fe5478e451283b848e1bc0f1d9d5f420363606b38c6c06c381dc4 +size 66074 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en.png index 03e7a81404..6d8014b510 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7418b3c08ffdcb58a7eee06eb86f9feafb0f02a0ff567d90833babef5f50a5c9 -size 60596 +oid sha256:de175424df113b1b0688509a23dd514679272c6641b64850ffd7072f2bdf458b +size 60443 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en.png index de2a58e79a..b916b4e68b 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fabf3e373947091f9b061cb1252769f9954a7394a39f98ce3c29665d46d7de9e -size 60554 +oid sha256:521b6f5002e3dde3ec57b0ad89a9c2415966cca80997962de95333c9f7585420 +size 60404 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en.png index 0f5dccae40..95aa10ca1f 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a4c600313b32c36153a90098f5da273d664aeaf6fc4de4b6be07b1e84a1f5b6 -size 54461 +oid sha256:e6cd97d0475b6dc1950d67be57a8cc9867d07c6428db828fd9a541118c8b3143 +size 54283 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en.png index 83f543fcc5..61dc44978f 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80c77888856e76935dee1e7512991287321a856c72f722298ec83fe54b9832ea -size 12367 +oid sha256:d261286b2d36c8e58290bef31d647214aad10dbac334b8151d8670239bf4e8f6 +size 12377 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en.png index 3994e5b8c9..1658700b51 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba14e3f3a756cc227fb6ad1fbe4ffea174cdd6e56ec982cba86b6025d65c7b16 -size 56354 +oid sha256:e553ac2b5b35f694a35f56948196f6f10612904d758b749c5878ca4ee2e1ec07 +size 56360 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en.png index 2bfbe443b6..af2737442f 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae2cd1fee72a1bd76862fe522fcc4afd942f763c677f37ec38198bb1223b593e -size 60836 +oid sha256:a1cdfbb66bef6d51807cbcbb75e8277bc9475d41f8ce95c5ad8440fa17265131 +size 60727 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en.png index 9ab5346177..f8b4c759b1 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28f0e05f1c37cfd0b47b231c213b9101f0cf4deddcdb0a3f600c105630b8100a -size 52017 +oid sha256:06ab908e6115f3962b9fdd86df644af55ef731898da958e1041d6113ac46e4f3 +size 51947 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en.png index de2a58e79a..b916b4e68b 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fabf3e373947091f9b061cb1252769f9954a7394a39f98ce3c29665d46d7de9e -size 60554 +oid sha256:521b6f5002e3dde3ec57b0ad89a9c2415966cca80997962de95333c9f7585420 +size 60404 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en.png index d8a24019cb..9ddacf0d4a 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:293daf287f9af7820b9117cc959eede1690df093d9e9e1b2008036b78c60ad13 -size 48712 +oid sha256:76cef9dd905c59735aee59b860360c12de6d0ff663cc0f40bb9a9f9b1fa19159 +size 48716 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en.png index 6f4516aabf..020731c6fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97fcdf04d36ac5610abdc12ef2bdecc3c4ecbf122c7a829603a7ab1fa6d6fe93 -size 52821 +oid sha256:ceaeffcb2bd4239d61e3c4f51d15d60a43626938a949dd5004a57ae35891f426 +size 52649 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en.png index 4e61663da4..97a84d0ce3 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0ff06a7d946dac97fcfbfa9a8ed9273915c3aee046e4dd5de5531d404d33ee2 -size 54457 +oid sha256:58768c1acb87ee8f74f077e84295d409713a54f6b91b37309c3fc189f913c513 +size 54298 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en.png index bcf0034ebf..07fe24d1fe 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3af0b02af1e4069d41d08256b252edc0bc6994bab9208144a46133f29ca0ac42 -size 56820 +oid sha256:e12b02d20a079766f858939c837878c6284ddc568d881585ca70321f6c4642f0 +size 56691 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en.png index 94bae794fd..44d6f46631 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d284095602bcd3fee4f63b2071de9b64d6a4694077167ca7915317574107cba -size 67211 +oid sha256:f1bd3c62483126552f1ac4ade2e19190ee9049c7aa05b3092fb0cc44d1d2c9db +size 66663 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en.png index 5b0252c2b1..da448e7437 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:881dadb37886ad8f1c2265466b38162941cbc0c4a5551f50ab7ac2f7db6b20d9 -size 61391 +oid sha256:b90f5e0c365d378fdbcccc0d8935b60a4b866ea2a7262ef5e5fe72316058e52d +size 61226 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en.png index 413b1cb404..3216eaa50f 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca4d64ba665223ff81b990571d95e0ee4fac62d80452e713be7d3e0b7f36585 -size 61351 +oid sha256:4de200678e33f38d5787cf569f46b3c0f980705b827374cb3ee77d0f0636658a +size 61190 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en.png index ee55ce808c..6da554e6e1 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43feb55b7bd4078957549d4c998b9c5c83ae02ede6222e7fdff522bb40fbe0a1 -size 54900 +oid sha256:1fcbe996ab969856de2c37da875aedbd3550c9209f58bc4eaaba4ee5f9e6183b +size 54697 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en.png index d3810d3da0..d808d93986 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:278da63de530e59035f6db74d46c81f1d26aa73649da0806adf2050cd67ab20a -size 12373 +oid sha256:2d7d643792fcae5b32dc2a14c738bb9f4a04c746adfc5d8abaa2aa1bae963a1e +size 12379 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en.png index 7cbaf82be0..7e9793abbc 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a41e7efce708581e0ab116737b59549b55f73cbacc8e1ff308332911d9042d8 -size 55462 +oid sha256:6ca4a8b7f73297d2130a0e7657b9abdfaeb47ce5e6c7da2c43105acf54be9952 +size 55449 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en.png index c855eae4e1..43a93b06ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed511d35b0a0bd4b5d1fa1720d87ef540f95eb1c07e7909d3bf8c990a1a537af -size 59290 +oid sha256:a116956e710f799768c2725247e4437d29876adfe92c584945bd1430e06aa7b7 +size 59210 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en.png index d5fb1cf198..451d85f21e 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fe5d748130431c697e11374acc7dcff18fa7d6d8c8b5aac522bec3d9f25cc93 -size 52078 +oid sha256:9d724dcb19dbe6b55cd5a0f0c5e9ba30b0e505be7d8466d5cda96412a1bc6c29 +size 52004 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en.png index 413b1cb404..3216eaa50f 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.roles_ChangeRolesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ca4d64ba665223ff81b990571d95e0ee4fac62d80452e713be7d3e0b7f36585 -size 61351 +oid sha256:4de200678e33f38d5787cf569f46b3c0f980705b827374cb3ee77d0f0636658a +size 61190 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en.png index ca58eeeb17..21a5549d2b 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b479f0c88766634a4c0522d6260f31a1bf2e9fc07018cfc2a4818eed19faf3bf -size 28328 +oid sha256:fff83d7065e2e9f60cc9eea91b3ba4eea1adde637d1cf51c6df01517c96879fd +size 28348 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en.png index ed38da4f4b..e3e3f4daad 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d6796ceb09d9e063097266b12f5533ff425d6dc89b1fda2ede6e049f1fe8bde -size 30164 +oid sha256:bcbe510bde44898e1967ba2b8311622afd3cc599794b300a57b4b3b8632000ab +size 30188 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en.png index 7d384e609b..0edb2f7b9b 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab852889a65d07e3ddab54a50c7d989c7ed761fdb4dd855b7f10d452b7840ce9 -size 58187 +oid sha256:5b3ab3cab91ff916f2bf6db476efeb9f809a35a602f90d0cf50f283a6dc0451f +size 58189 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en.png index af2faee64c..6ed5992b27 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:230e766156de551fed6ec641e813a136c3bcbe1160828777623a2198eba194ab -size 27747 +oid sha256:f122ac9e7601af3b03825ded5eee90a942d4cb3415b479b0c603a7664c12acbe +size 27894 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en.png index f1032d7bac..28089c3a19 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea23ae24e9d7d7eba6cd75e009129a10e90311f2efddf0a4d5781b97d944184e -size 26718 +oid sha256:c48f20baf7ccead203a15ded178a46729d83f492414c907e67956f869c14452f +size 26772 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en.png index 59b0916092..6be69c6fdc 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b124e86fcfb1c1fd6aa04200065bd90966a32ac0eb66aff71c5cd9e362660939 -size 36693 +oid sha256:07ea854f28bfe2328d160739572ad8f2240823823780ba132be39786421ef45f +size 36742 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en.png index af2faee64c..6ed5992b27 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:230e766156de551fed6ec641e813a136c3bcbe1160828777623a2198eba194ab -size 27747 +oid sha256:f122ac9e7601af3b03825ded5eee90a942d4cb3415b479b0c603a7664c12acbe +size 27894 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en.png index 5c120513de..707c03e70e 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c73af7aa4ee7b60969a0bfdfe77494ad6c8456f3108450605242e41f0e50f90 -size 27053 +oid sha256:de6537fb8cab6f228c2b837c1833e63bde99cff5fa7d5660e99e0bd6c62d34ad +size 27105 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en.png index d14022b8d4..05e024ff89 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ec56279c2c0c390ffcf90798f121add48d1afd64e6cab96a5e0af6f61493d63 -size 26655 +oid sha256:5481e49a79c7f5bd2698ee44df7fc78aa2a7cb8567c9ab6f2c28f93f36ee60b7 +size 26620 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en.png index 0471e73a55..750272f011 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1ed853f9fa3a58d4f1fbc6f028264029b76b53fd5a7baec1dd46f694766457a -size 27456 +oid sha256:c2da90677e466c2856fe59aa0053598c163fe33bcb673672f58d4d9a502126de +size 27556 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en.png index 6d29fc04e6..8de25ea721 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6f34de05ed23c58362015cac39d28917b6735872c1d1521b43127a283078cff -size 29121 +oid sha256:e56bcdadd28809627bb3f5f0557b37be511a6369ccd0a9861e77cc2337a0a5dc +size 29237 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en.png index 895f52fb18..6420909c90 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bd29696b70564a9f78cd98f957c671faaa450ffbb9aaba37885d50c729028e7 -size 55526 +oid sha256:e1a4458211c4158f95837f3d3388a770e8e092ba43d6653b616a57f5030702e8 +size 55537 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en.png index 83bc1a6fd6..78090c5620 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:610649cb00a0818364c2dbd4ff70246886a82ab0d0b21d0dd8e82beb2da54981 -size 26677 +oid sha256:1c5548cc4e398f15a7ed1118b78520e793e358a67f1d218d003b03f1be13aff6 +size 26751 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en.png index defed0d920..48a711299c 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91747927961186377681a43a5190ca6923409781adf833d61e547ed133ac75e -size 25179 +oid sha256:c23783e4367a8e732e3f3d528842670a4e110b26a9866fabd8598f382790c1cf +size 25211 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en.png index b4c18aeeb7..be1dba1967 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66016e7926dfd7db206c8695dc9bcc2ae0ee40700ad31f7c00f67d1ff425b761 -size 34479 +oid sha256:3dc931893c2233a6a83fe68bf8712ac0793916298978fbdd41c481cfa63eaa98 +size 34495 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en.png index 83bc1a6fd6..78090c5620 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:610649cb00a0818364c2dbd4ff70246886a82ab0d0b21d0dd8e82beb2da54981 -size 26677 +oid sha256:1c5548cc4e398f15a7ed1118b78520e793e358a67f1d218d003b03f1be13aff6 +size 26751 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en.png index c05bf83229..893fa03361 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28594fb95b9607e224279fb03d06961faa80618adafde5112283ae84a67daa7d -size 25556 +oid sha256:1fd326fc0c8e70625b792ee1d058c370401820ba082aebea938ebf092d200a14 +size 25584 diff --git a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en.png index f2fda142a6..b64cee9c22 100644 --- a/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.rolesandpermissions.impl.root_RolesAndPermissionsView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b5c5abd858c9b78a9f354e81977d9fe370dd511c242d75bd02385122101c766 -size 25826 +oid sha256:c2518b9917b3eaf7381a1dd12d6f3b3aba65ab678fedb369889e8f76d609782e +size 25750 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en.png index 24aa702c79..acf2d10b4a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acb77b2b218774a4f19537af5658205f2ef8150355282d1e1d85e68888712144 -size 31578 +oid sha256:419fa8d8d1e5290bdd2506824663c995cef09f52c9b4a9a52926b6cf94809b37 +size 31620 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png index 0274af2d42..366bf36bc6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca592ac12d7cb8583962d8c0d5d99a57396ea807f830c4a727fae5ba077c3a1b -size 35709 +oid sha256:b5cbfb1852861ab4ba92355cf0c3e59919e2209338cf299cdae39791472674d3 +size 35398 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en.png index 21f4043acd..23935dc73d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e732b8e2c65c05096fad71211c8cee185c20d25386544d7245211f10922d40 -size 30159 +oid sha256:91f38a465b40119727d4a0995ee26170d35d128bf485dbff1d6aa263e5f17513 +size 30303 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en.png index b3082edc14..7c63030292 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8ce8c575c2b6e313e6623e72b772a6ee5fa175a4bf69a1f17ff8628cdfede4f -size 31489 +oid sha256:836ab57eebcd04857028dea821380b0d8b46964ef2775bbeed0e4ed1a6ee05fe +size 31547 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en.png index 21f4043acd..23935dc73d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42e732b8e2c65c05096fad71211c8cee185c20d25386544d7245211f10922d40 -size 30159 +oid sha256:91f38a465b40119727d4a0995ee26170d35d128bf485dbff1d6aa263e5f17513 +size 30303 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en.png index b3082edc14..7c63030292 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8ce8c575c2b6e313e6623e72b772a6ee5fa175a4bf69a1f17ff8628cdfede4f -size 31489 +oid sha256:836ab57eebcd04857028dea821380b0d8b46964ef2775bbeed0e4ed1a6ee05fe +size 31547 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en.png index 24aa702c79..acf2d10b4a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acb77b2b218774a4f19537af5658205f2ef8150355282d1e1d85e68888712144 -size 31578 +oid sha256:419fa8d8d1e5290bdd2506824663c995cef09f52c9b4a9a52926b6cf94809b37 +size 31620 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en.png index 653b7031b3..64e9b67cd2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dae1b0140dd927945df0e9abc143b15fce938747bb1d94759a9d6f198ebe20bf -size 30598 +oid sha256:c1639b16313e7c2e552fe741dc5af84c946bb9bef39fdbf98f3a5b473fa8de1f +size 30651 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png index 57bff77a51..7aa4fbc6c2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56c928d1868ec3ff1f251d532b532397f688949e8cd6705ef363651c88221019 -size 34350 +oid sha256:12374c4bc5ad2290b98319d3301ba365d6d3d3d3c74e3705a621272179bd149f +size 34316 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en.png index d9b229a745..c44f3259d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1e1cb86dd8b566d3dc4dad42a30bb89de9fb6990d36572589ec5938e8411a6a -size 28940 +oid sha256:d935d8a9e4254d5f7220576f2c3a3272d2668829ab9f0cb0b7f6739a0b52658e +size 29037 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en.png index cb74a59e91..b2f184d679 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaa802a8e16d04b622f861c84b2454bcd5da52d27497972490a4eb7d9970153 -size 29554 +oid sha256:58d71e51474d4247b855e2155a287ba7785e25f2ef962bf11c0ad4967bff3f35 +size 29562 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en.png index d9b229a745..c44f3259d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1e1cb86dd8b566d3dc4dad42a30bb89de9fb6990d36572589ec5938e8411a6a -size 28940 +oid sha256:d935d8a9e4254d5f7220576f2c3a3272d2668829ab9f0cb0b7f6739a0b52658e +size 29037 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en.png index cb74a59e91..b2f184d679 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eaa802a8e16d04b622f861c84b2454bcd5da52d27497972490a4eb7d9970153 -size 29554 +oid sha256:58d71e51474d4247b855e2155a287ba7785e25f2ef962bf11c0ad4967bff3f35 +size 29562 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en.png index 653b7031b3..64e9b67cd2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dae1b0140dd927945df0e9abc143b15fce938747bb1d94759a9d6f198ebe20bf -size 30598 +oid sha256:c1639b16313e7c2e552fe741dc5af84c946bb9bef39fdbf98f3a5b473fa8de1f +size 30651 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsA11y_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsA11y_en.png index 8d8186f2d3..232eff8a27 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2119838c9649710465dc1b8610550ce101f3016a1a6511c3f6dadc715fb75862 -size 82975 +oid sha256:dd57c6da7471850cbf9d4fd749b249589cd1dd71eebeac50fcef8a1db9efc04e +size 67420 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png index d674b1ad00..77c11f54a2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ad7b9331a5d504b8d2145c4428e6b64ed838b371f1aad5c288e5545f8a20f1f -size 45464 +oid sha256:a78e7d4a345b9beca19dedd29282f9a891ceabdc05d203cb4091790924b0bbf3 +size 40335 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png index 2567512ca9..5764767fef 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d74ede198f221b1138ed49bac7e02ee0fefbccd38735b09237fda95a78f6bd2 -size 44174 +oid sha256:29304b6e03fae0031e6fb536ffa038c436b224cf6c2fe095af1509d8cf1729ff +size 38520 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png index ee9fcd2088..d242f48a87 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3f483a9e05278928f806f9534720c121efa77bd11b93ea6afc8b543f51a6d7c -size 43171 +oid sha256:966f9a73b258b8b0036255d2519c64a306c07098001f9a140d5a3af9a7ed7734 +size 38088 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png index 724a99374d..d3baeac29c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3045102b9695ce3c095ac81e3d3d26dbf182d433427903e280189613a74f2984 -size 44758 +oid sha256:44fbeb3efaad275d864b9cf58075fcc1df67c49f5498017e53a6c203ad5d0513 +size 38858 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png index c62a4e99b7..a1e27998fa 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bbbf23895558745ebe4fab7faee8ffb4acba07f48b652cc130c78ddf89bebef -size 44684 +oid sha256:82f34129e77fb9e84c513a950b7c3d2e20af376f357135cd7bfec67ec0850b62 +size 38790 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png index f23e6e5532..fb02b6a6ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f72fb044fb52f226600ad2eb3344e8122edcce15c75800fb483f3b93a1c6e26 -size 45092 +oid sha256:cfa7cf1c8e98ecf02f00a20096e5edd5a720050dffd8a359335d479493b40f50 +size 40212 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png index 47f40ccaab..1da1c133bb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14261c716c286b17ee2afcb652378f9703b39f67b219256f6ac3cde61cd50947 -size 45589 +oid sha256:8f5be262e881a33b9ad1425164ff8424ca72e12a276918f173fd3ff5cffb9ed5 +size 40555 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png index fd5b8fce76..645a7ac9f4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1b1c3f8db4dc58dcaab91f254da2906138c09c65d4e57a77a5be9af371aaa5d -size 44944 +oid sha256:dd555ca1156752b9d07d808702c2e7bb04af00bf8240947e1cb4a7ffdc4814a4 +size 39066 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_17_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_17_en.png index 5491a33e2a..99451027b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5d3a4bd5c340c0d439ccd7d7e24f372d89255a775a9497e5d9b8847354cafb6 -size 44236 +oid sha256:8630ba0169ecb20ed5076623fc9a6bfab6d30c75c8040958fcbe3663661f5cf1 +size 38502 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png index e44f676f01..9647ec0023 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d9a464a1bc24dc76d1a10ae835fb24839b521ba446a1adad3e70f4ae5f17857 -size 42076 +oid sha256:57d4c23cf1f13899370c0bf0c99e23b0113039dd50a8fdbc1706df7691943044 +size 37354 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png index 46bb9b0a89..b786e48fd2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29592dcede3af29136c857b03975c2697470682371ac5d606f2e6974f89ea268 -size 42030 +oid sha256:9256ef115348a6fbcdc5bd275c7dd97c5018b890104c1074b65d0eef6d2ca319 +size 37334 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png index d58516f709..9114e9a598 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:676908cf3eabc3417f228dabd2663bb0aa3d903d1b6f0e2fe770bdd6fbb48af2 -size 40407 +oid sha256:9c4a343ba9e0a8a6c75eb9cc54c4fbd460606de184c8078242a4e06eef7c3304 +size 34641 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_20_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_20_en.png index 7cc47cba6d..449946c634 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_20_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c66ebe3c16933495fe33596036363f95e20654b6ef5ee822f594a34531ea640f -size 45211 +oid sha256:ef52fdbd2ed8efdf526efe2fab402d7100952522b7e01023ec73e10415e27700 +size 39183 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_21_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_21_en.png index 8d4880b59f..b10f7d38d0 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_21_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_21_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de5ccdad3e08ed4fd4330697ad6c7085d5553cb889b7b7c12879aaaa6b34c1de -size 44953 +oid sha256:0215a5c510ebd2b99176525f7f317654e3c3b2fda4f5599c63b589f5c6286efc +size 38951 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_22_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_22_en.png index 52e66c38c8..5fd4e0e5ca 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_22_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_22_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e215296d864d34ceacaa53e7fec452643b63d72b253074eb50617bab50915ad1 -size 44670 +oid sha256:bf37f6deee46b8949ab91e88c49811013876307009b9c84ffaefd8229034a26a +size 38837 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png index 2a765938ad..96f469822a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae13e58b01e0e43591c0374474b33fea60574a5d8b22cb57092d15637dc58baa -size 37920 +oid sha256:569c1c17bae3b57d21f04d9ffec1ee91f7bae1ee773e75e7418850e9f83f3281 +size 32286 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png index 8a7f2e2d75..557f249715 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0cc874a4ba2b8bd0f0b7e290d0ffc67e82d1222fd287360c3321143d5f87ad4 -size 42369 +oid sha256:6db7bc187b92d8fda605212e54168649f1385f7fd5d938496510289cbbdcfe63 +size 30729 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png index c8d3e8f6fa..240291e5da 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5bb345d25a8b90a34708123fc6ba72dd6aca2c09ade4c26b29e9168d65e18be -size 42466 +oid sha256:8be6501b50d413f75e860bd04f03803cc13965a991e7d64019e1a7e39a0bbf2b +size 37416 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png index cdb5f92f18..7ee59b0d02 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1d4af8779d9869938b17f26b155c584c10d119479c62918d5b819a0db30acac -size 41720 +oid sha256:c56e98e7ff452bd15b7a635d748815b1c8dd5490b9309e09a93a3cd4832db660 +size 37066 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png index 1e0b3070ee..f30bbd9f27 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fac98b00a99771b186e8146b98ab63602be7b4c5cc9febc3ca821c1d5cd6a1b -size 42700 +oid sha256:a10328f429081b196dba40ae60a6100f2489a60a2aab37388deb645549fadfcb +size 31485 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png index 478f3dcb3c..ea46034c38 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cb0953ce6c69ad2e49213bf0dbff76a87505f88a092660b71d97ad1e75a5344 -size 45766 +oid sha256:cb772e96c733fddf80ba6dda94039d971b1541a39ae349b3736e1a272f0857b2 +size 39440 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png index 37a67e7aa0..00e9e79aa4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0f655ed143e7b894e37d99fc64c75d44e6483e830055e04609a9da36830278c -size 44670 +oid sha256:cbdce666db6f4fb2a35f854b3e3c652dbbf4c0ba798aff3f5f7bfe0da4510950 +size 38841 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png index b41099055e..7530f500f9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac26ca718f2118d88315704ce4bc7960983ab2ac0c394199da2c5b3feeec4438 -size 44688 +oid sha256:d9fbc551023fce72f97c6f562306e80e4e43bf83ed35ddbfcd86a1a3053c1545 +size 38936 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png index 836386fc75..f63ae1fe95 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a9c05a4ace7dc7013e4b02b5e652296399cb2f391ed4bacb2f74d06b68c4be4 -size 46436 +oid sha256:3b7a58ad7ba5f3a81f0b06a9f464f91ec3b4b39fea3e4db6657bb9bd43f80321 +size 41501 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png index f3ee29437c..d292c67bfc 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:997444daf4024495291014f1cae1cee2ba389d160c0ccae706b120dcd908996e -size 45024 +oid sha256:5f92b85722afbc65a584f6e6f064fe6b3b38a888965d97c569c727253f4cc7d7 +size 39519 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png index 5ccda85a79..f36d741372 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:527d2c4574c8a5f6946ee94415e30152fdd9e153fd7651b5a40df789e8694d72 -size 43969 +oid sha256:77e3e9dd2ec03e1a4a988e25a8a094d96296149ce7b6dc1d319c71b84dabfb2f +size 39357 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png index 60753106e7..069df089eb 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7da6bfdf7bc9878e1850f8a443adae82a3a4b0aaa5574cec74081ba6f88b879e -size 45622 +oid sha256:8c182019343e29d186a1f2a7551f82573999a26d12c77c10ee2563aee44045d9 +size 39893 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png index 5549792942..d8d659411d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e253e47f998774da8443926990526105fd918b79040955a3d884558a110db8a -size 45538 +oid sha256:85e5490ba14a46176971c2c23eec05d352e871463eed9241862faa9929c11188 +size 39837 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png index e5dac7dc72..01041d80be 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60a989450ffb8ed428d5dd021573b821d238a06f5515f21f9d50b855889ad985 -size 46022 +oid sha256:c07ef87165b3f4e13108882f108cd3f217f3c6f8930c703b8909f431b2a9ed2a +size 41337 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png index d4143895a2..5a90de8c66 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37e6049854cd5aaeb50f38c4604e631fb561f0f309d4c58d41becf69a01ca036 -size 46596 +oid sha256:19dbd2929dda9108118ede7627241db2249f8ab7a062f17bdc3619ff42253594 +size 41768 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png index 03bcda4666..3d2700e9af 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05a841f7590a33ffea5b09ffc0975bc9090b03cace9e22c330965ce773431211 -size 45849 +oid sha256:c87ff2cbbe4d5912cb17fccfe5fa7614d5260b43ada3b952dedee57a3c004cbb +size 40117 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_17_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_17_en.png index 19d509d513..10b75e049f 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bc90cce90b0648a340e332276fe4395aec673408aaf938bd0d744d45c89dec2 -size 45396 +oid sha256:77359209da8ffbdd933d123a1a6952da961c75b3ca6a70b6d49ee9be188fca7c +size 39670 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png index 8027c68a7b..4cbe816fd3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_18_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3ab8eeb4123df1feb35b4d9d9c7fbf166be943edfeb9fa23ac6549463763b10 -size 42853 +oid sha256:a7190a790a208709d20691052ea3de64a359e7ca16d8a988d0ef62b33a64da3f +size 38756 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png index 64acd4e843..8eeffdbcf1 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_19_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3eee4e61c8676a547c3ccbb1d6b9ea2ce0ae30286012de6b3a4f912b908d2e0 -size 42722 +oid sha256:15458edded2d32980c16163e6674d714d67442a2421d0af73594590cf6f144d3 +size 38718 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png index ef06cd3451..c4c2f1822d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02c734eb60efab5050988331527aae294fc6baf0b110e3395103429a79ba059c -size 41424 +oid sha256:13ac1bdcdacb72bd268cf00e8e754f5746928723891fe1f70277daa46f67ef1f +size 35720 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_20_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_20_en.png index 9462f1336b..c70dbc195d 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_20_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:389fcb65c5334f31b3d66731de2c65128d0a17e7380ca7c45be8a36023719732 -size 46097 +oid sha256:b4b9ca327302ed396498c60db4341c78e3d48079e04bfdbb4ce8f024e82684f8 +size 40307 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_21_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_21_en.png index f1f9639eae..3a06126d05 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_21_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_21_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:567cefb525795b66704ccd1738bfe08247004814c947f6c60801588304c768c2 -size 45835 +oid sha256:f4c7a00882958b916e99ef643cd698229f8659fb8f21e4e2b86db1432362f508 +size 40007 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_22_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_22_en.png index fb830369a5..e6ccda4710 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_22_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_22_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f87512e18ca91237141d97ed03f4725a8a671268daf50f0a8049ee34bdc42a61 -size 45503 +oid sha256:a9bab5037912d2bdeb85ed013e169d98f5f7333c274430d69667e32dfc72c1fa +size 39880 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png index 25bcd31599..0f9431284b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ced1cdb2e11bff01ffce233ddf6cf826d6db266739abcd022f83b58d8bec012f -size 38785 +oid sha256:c074ba644aa7ab073e2b1893a2f82a8e11eb479e8f50ab9cbd6e991709cc5d5e +size 33376 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png index a64f6a2843..56cfd78cd6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfd96490ce4509620efd7be72e6ecca219eafe73050417c0c3685339444457f1 -size 43060 +oid sha256:a07f005c828af6a622527a062e6cd2d8f4120ed4ac4a63daf675cfaf1b36da11 +size 31486 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png index 92b2b86922..f82973cda9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db67d40bae68ec1ece950a06f618606fedc78aa997743b1804db4b69848ba65b -size 43381 +oid sha256:ee239549a179ce7678494cc20b32db88a211bba1f6368ba17fa1b18ca3400810 +size 38586 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png index 8b143f6b5c..a37eaa2ae6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cee9f291a3257f3960ea5d1490f3f219bb0d42b01109b9b398b941524b85971 -size 42383 +oid sha256:b56916b6d08aa4fcf55ee414ae928946d5a2bc5eeb610f9ec4ac86cf783aa11e +size 38429 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png index 2f72417b73..f305d86263 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd455b5b492eb88bde4e985ca01ab97e23a337d7ac478782c09dbb10803960f2 -size 43430 +oid sha256:b576b33a5c3bedaa8d1e7df6e95541730559c8149f5a0d920c9245449b33c26d +size 32616 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png index a1071426b1..964ccd2680 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79ac979bcc0339ed30c68c4617f082a9398edf657cc3aea1c80df04bd4b8bab2 -size 46724 +oid sha256:a7b1c29bca9904fc55d17356c27a8dbe035e1fbbf0ffa27ee2b466cf1558c418 +size 40573 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png index 36a3defc1f..2ced0a9e17 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d71c5e3a8dd2defe2dd37fee1a22921f49bbc9a52a766e5ad082b6595c6ff75 -size 45577 +oid sha256:8ad0f0df59f4231bd85a0ae99ad6eccb74b338975353a201f434e5047fd58628 +size 39963 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png index 6c4e522988..9e24db9017 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ada1e17913092a5e33b5f422a41826dd711102cfb5ab396d5d323e8e0f0b6589 -size 45590 +oid sha256:616b6050ca126b0630b5283901afe4096f7d041a40a5d6ae8e64ff808f2d4e7d +size 39971 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png index b5ff7ebce2..202f8a06db 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49ecd29e87c23ac92f0b5775e95a1b46dda41bbd7bb52a5cbfbcf7cd039824c9 -size 17240 +oid sha256:75880628a454ca00a186e5d1c5438117e7af4ab9b23eb6fbf9c9696210234169 +size 17246 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png index ae186ece30..9186ac2de4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ac03bbed84ae266088d0e487e36ac30ddaac22bdaea6218423a8a41b77f4c5b -size 20217 +oid sha256:1fe61c20e2c6a7bb72e82aefd546469dfc49bf736b6d12f4c1927628df1bdb3e +size 20211 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png index 6311df6e6d..ca8464fad9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d44e5119301f217903d1ca232f17ae54ba96da9a6878e668f603c57bcc23761 -size 22612 +oid sha256:ca3ee9a09bbbc69f2ba7e8b40cdc5196cc53745fe3222282d98bcf948dc16745 +size 22596 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png index 90b562aa09..945743ff26 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:980b9744a36174bb8b080a431b277b544dfc045869102147fb9d1186e396b5e9 -size 22720 +oid sha256:96971a8981aea8918c01827403933c9e75275a2fda92716f803e6f28ad284bae +size 22702 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png index 46dead6c87..eb96c3e39b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e611504d154db3889c1a5900e6af392ca058462450d66f0dde762ab4a45e1b76 -size 29532 +oid sha256:06ca0aa653033b59313325ad508c5916e2fedf29ba6dd3a5f714e90d01f4bf7c +size 29644 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png index 73b8b8e50c..5c597300e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8a3ad99763929d2783147814e44fa9ec7e7efb7b99949e3580f23c38fb5d971 -size 9398 +oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5 +size 3853 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png index 9399857173..9b9542917a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6135b24d94d7732342ec9353cdefb115ce5e9a799dfaee5d57b2e8e0a7ed631 -size 26505 +oid sha256:ebfb75986cd385e9837a24a5f279e07f0744bab5613fd7cbaf0a493d021c7699 +size 26606 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en.png index 8380d60ccd..18d1be85a7 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bf31df95e781eae5530ed98ab9773f3e7d0f4fd4152226596b78328b88d69a8 -size 27223 +oid sha256:a58e6ed7f10d7014c9c3b55fbb769e04619313212449a8fc582501d2904d669f +size 27315 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png index 1a59277355..c1b0c4ff1e 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ebad5a2935050fa87c16c518323bdf3db4494fd8a14a5a03809e3e51f047b70 -size 16318 +oid sha256:bc96eec2d61713f01d43c92812bbf27bc44a202ce55f0fa1dc8e8659dabcfcf2 +size 16347 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png index 9e8644ac62..16b567164c 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b427494aa12528f795a49f27912b90ebb59392be0420fc27e3c4014a3599680 -size 19155 +oid sha256:56c4df7da32b71dca109a1db1f4a797a2930e3b42c00eb51d72f0e1f5b99bf48 +size 19200 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png index 01b60a339f..ec10d3fd8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d301576daafc3b86949208e281479183558d61cbadeb412007ee8436aa7eebb9 -size 21590 +oid sha256:592c6ec197b193803679eee5df7769fa4858fe883a2f3a1294f39ad87f9f4ff1 +size 21610 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png index b96d820784..5ff44ac3a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc69bdbe8ac22e7d6470a8b547ba364e3421f6d61ccd0c364492dd860b1ce093 -size 21682 +oid sha256:62e99b8070a394ea5b2d666f3a8abf0fe41ea4a34a382223903f2de4085195dc +size 21710 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png index c5d3a63af7..2f5afc02b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:641f434040dfc9f040622fd0f17519d353f0bc45a17603ef837e53fad2b529c2 -size 27570 +oid sha256:793cff43740f6e477a255af566985d8bb7b0242d89484b3f0fc5cfec3b4a2af8 +size 27608 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png index a06978178d..4a9b30b4cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d9c0f0c69bd55b5c9a925cb89587d959998a833c5927873b4ef358959ce4ece -size 24944 +oid sha256:ad842be0c519bfd7713eb93df2a7216d159970aff1ea44725641c5db26f3ad6b +size 24995 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png index a87d89c701..d47bf90664 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f3a0cb5d9bae6725a1c527f3d9575aabf85d3edf63a28d9faa23f4f0b65e2d0 -size 7624 +oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c +size 3664 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en.png index 4c39cde004..4964d7d1b6 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f17c5df90399b6de382af8111ba36b0ec072596f42103f76477692a801493799 -size 25515 +oid sha256:541b9ad607838fc9d44d6d3759d61df54c178257fe646366304d96ebda5f7e77 +size 25565 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png index de42fdfe94..fd32767a41 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15f9aa77f9f8fe09a10e2e22b194f2ba505d56c9097db0872b317840c0fa84c4 -size 28661 +oid sha256:218a8bafb58855056ab07b05d45c87df482483c02a5144c0117c1159135e924a +size 28637 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png index 6ded082e3c..a3c91c139d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1762442b6dcd37d5710437239055e7d40d2b741cacd1045a3eacd327fc12ee -size 28053 +oid sha256:76f1e9db9260cb23bd80bce5f45bdf323a8d24cb87757d7a32f9c6e6b632913c +size 27993 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png index 6ded082e3c..a3c91c139d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f1762442b6dcd37d5710437239055e7d40d2b741cacd1045a3eacd327fc12ee -size 28053 +oid sha256:76f1e9db9260cb23bd80bce5f45bdf323a8d24cb87757d7a32f9c6e6b632913c +size 27993 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png index 88df849ef5..6c8ad3018b 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea5df9ecebf686ac65c265d1b74e3a0a5169d1e308eaaf4f9c205a754d8bddcc -size 40150 +oid sha256:4fda40268a68acf664e90a48391658b8dedb3d7f5d5e6b76d680ab25180d5a55 +size 40121 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png index 58a01b25a3..4317869283 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06170ddb862b337c00d50a531caa1f673952cc0194cb38410d3aa7cc61d95c16 -size 27653 +oid sha256:6c8f9e633578f46a6b8126ea6dcbdb5c113dd0e1107fbc4d5b2206c2f5acfb1c +size 27835 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png index f346d344e0..eed9b50326 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc1a0fd307ba6ea9a9f578527bae5a074e5ad67035201d8aa7d85877ee697af5 -size 26690 +oid sha256:d55afb5f76aac3d1a8ff0f1c223502fc798dc0684f9d84df79256c41ca5ea1cc +size 26706 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png index f346d344e0..eed9b50326 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc1a0fd307ba6ea9a9f578527bae5a074e5ad67035201d8aa7d85877ee697af5 -size 26690 +oid sha256:d55afb5f76aac3d1a8ff0f1c223502fc798dc0684f9d84df79256c41ca5ea1cc +size 26706 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png index f38139bcf2..0b3c962498 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:763ddaf10dfed74dbef17deccfa7037d8b643e9b8654409ce96e8192ab5ee38c -size 38418 +oid sha256:9410147c7a6a496fbd356b0dbc3b14998f20ddea6a2eeabb79b9600485285a80 +size 38628 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png index 1fe1e31cd9..7575635c8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e38857c0fe6e6ff461ace5cec20fdf6d82caed44b0f554910fcc5dc4676e6748 -size 30468 +oid sha256:198e6320c42714d651e1c0a770efaa620dca94dff435683a34f06c2e1e946a4b +size 30480 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png index 1fe1e31cd9..7575635c8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e38857c0fe6e6ff461ace5cec20fdf6d82caed44b0f554910fcc5dc4676e6748 -size 30468 +oid sha256:198e6320c42714d651e1c0a770efaa620dca94dff435683a34f06c2e1e946a4b +size 30480 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png index 709cd8354e..d9747b1991 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aebfcbfed290def2f0d9c04dabfd9ac0e3bbbacedcad9533660583080424519 -size 60014 +oid sha256:3b46afa99f390d90461f74ca3c25acc3a5a9a47167fc828e2d7bb732073c4502 +size 59824 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png index e12dbcd08d..a004a503a1 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35b333fd03b952f4edfa724382a489f3a84b7a010980dac6e9e9dd0f56bb69f -size 60442 +oid sha256:d776daac96caf933c8043f4525a7a3e10bd7df82375fb841e876f3855a58d82e +size 60289 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png index 59c7730b5e..4e20779219 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2da1bc59e3f504073ed1875640d8fb696653dd70587161e97d2b738c1c50a7e1 -size 40954 +oid sha256:907f6e63ea0975f47ebe9bcf6211d54910d30bc03bb81a966d0af11f3b8cc410 +size 40967 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png index e31324db68..1a9570f98a 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df2e1fb160f4fba1418c401a6323132ecbbb8718605704d475336547356aa609 -size 57377 +oid sha256:c01448cd985faecdee7a30b2a9195d80a333e874e47ac1d96f91f01163c5b911 +size 57420 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png index 00db7b8791..a011ffe8af 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2203bd270e716624b93f25adb37dd71afbf25a5590e72e65ed4e53827355f02 -size 38832 +oid sha256:7b36d73ccf855d00bcb85772da0a295efc4982bfba45867373adaae9cc1da086 +size 38816 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png index 4d13452bf5..4a2acf121c 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87dab9cb3231c932913d446c9b60515a31394f7c2ea87093aff460a75d1f85f5 +oid sha256:a92bbc343d11f32019369bae97c582f1ff0e6d867c9bb85b8b2f82421cc0656c size 31738 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png index 51a6988af7..80b46c897f 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06e4300605e15dcd7f921c2bf74ebae809432a0b723ce986d18b853144ef9de0 -size 30557 +oid sha256:6b30fb3c864ef609f3e6fa861bb77773f25587ccdf77ae2dcfa71808f39474ec +size 30571 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png index 1fe1e31cd9..7575635c8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e38857c0fe6e6ff461ace5cec20fdf6d82caed44b0f554910fcc5dc4676e6748 -size 30468 +oid sha256:198e6320c42714d651e1c0a770efaa620dca94dff435683a34f06c2e1e946a4b +size 30480 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png index c87102abd4..d846e3547f 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25a0537f0b46efc42176e30bfa3f9e789b0b53612fe6cf683270ad5c5b51399a -size 32105 +oid sha256:2e1c127d917fbc8a655fe1f957b879472bb940c7258c33e406d5e257732fb9d1 +size 32093 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png index 1174143a80..2daf914755 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf60051d705987828e0bc5c1cbf99b3a65c792fdcaa205376539c8461dfd1177 -size 31381 +oid sha256:0cec64b7a5f5762717821cbb10fccfff4f06695a88d9404a08fcc5b51fc96e3c +size 31382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png index 5558390316..888bcc72ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da05500aaf5ed3adf5f1057ff3ac865c12a9dfde77791592425260f3578d1161 -size 29619 +oid sha256:507a24f95e364976360e03baa3a8c7fb9aa47ae013e1c299b984a757a83c6211 +size 29627 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png index 5558390316..888bcc72ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da05500aaf5ed3adf5f1057ff3ac865c12a9dfde77791592425260f3578d1161 -size 29619 +oid sha256:507a24f95e364976360e03baa3a8c7fb9aa47ae013e1c299b984a757a83c6211 +size 29627 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png index 6dc75b18b8..7a6b531589 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1dcd0009809a127c1018f6e942fc90baf78c7802c86fd384a273f51483a1706 -size 58048 +oid sha256:eac4a38a303f4d590bcd98d5f82402234009d4929d42290661b9a388972dedf9 +size 57879 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png index 18b9b26a65..503ddfa0b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66e2230268a632877b50fefe34ddefcd6f9ff31a34b3107c420c36b12d7d0f83 -size 58539 +oid sha256:5b87f75ef0fb631db93894aa13b4a157b70d73a3143f8a6efaba6e5fa2993856 +size 58587 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png index 8b3123e8f2..4e00fd1711 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e4cd430376a28e6ca20082c328d1f15953b5ff54e00d99cd49078eec9e62a54 -size 39883 +oid sha256:ffd6e9fbe283e921d5db9a6df46801300e0d1ff3879694203520491e4f7e33f0 +size 39907 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png index 77d0152a6f..3f6dfb400e 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b69d7e7875b3b4c332d683b121902f88ad217641841f72cfa9e95a64eec53a5 -size 56252 +oid sha256:1ae027d2fd14b2fcf5ce1121c0986f4e54f1156d660d16e667b8d176a1ef2e81 +size 56365 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png index 792574e392..ced01412cc 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45d4a147b71606727225be183a67b780dbd01a2c9301952c9dbd8d712da8b56a -size 36418 +oid sha256:ddd31f317fc31f901a25a55b95dffcdee36a054e69230a45fe8be7179b77fee5 +size 36405 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png index 5efbc25fe9..779bfb91ff 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:227509f9ae8414add5f091cde3cc068aec0385674b09c5c985b1e0758bb759d9 +oid sha256:e15b5a6bc261e7a5f579ef87d1a6fef7896f74edebe4bf01ddbbdaf4439be34b size 31010 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png index 89fb6f32c5..fbe7eae12f 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa32559c70491ea4bb7f315e493effe205792d8a7186e4ba8e484d31c109eae6 -size 29733 +oid sha256:622f8a378608576d7491597c6ef50e9cbef3e9fedf3051339c4891357e70e9f5 +size 29739 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png index 5558390316..888bcc72ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da05500aaf5ed3adf5f1057ff3ac865c12a9dfde77791592425260f3578d1161 -size 29619 +oid sha256:507a24f95e364976360e03baa3a8c7fb9aa47ae013e1c299b984a757a83c6211 +size 29627 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png index 3f191818d9..a792c8206b 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d5c5c44ca5d5976300b5e580184cb414a3c1f8f49dec9cff50fc0ab6b749fc -size 30226 +oid sha256:d921b380e05063273f52c4d7fc613f464a43ad1dd28c8aeda6ec101f653f2fd5 +size 30224 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png index a720a81a0c..aa48b9f158 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.root_SecureBackupRootView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92252579b05080baf11b2b5ae7b761e1225f9648365800c30b50283e7fd5afa -size 30497 +oid sha256:dbeeb89350b85c5ca23f9e4d8bb980bcb5620217150b6434d2b4c4d1df4e74e9 +size 30498 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png index 21b17d8656..e1573dacb9 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:562e8457321ec1afe9ca9a2111ee19815aa25401cd925c0bcc880a25db99e0c4 -size 42100 +oid sha256:01d03620cae8382ee41703d7553913fbd1e463fcd55fd661e9b99087e14bf6b1 +size 42162 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png index c26e5c44c1..bea6d32d38 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4498edc796206821f346a2dfceb966f59f4b7adf513ae184c84d20b323ed0d67 -size 39800 +oid sha256:54d9e8714cbbc3785ccd25d6bff9c791d57acea007d6cb8774a4a5d61646b54f +size 39857 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png index e798d07cac..dd9010b013 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f3d6bcf6b603ea2e2a619f569960438337fe9e2ae9eea2657587cd211bbf278 -size 35444 +oid sha256:9f6ce15f59eba49738b9df2117764e760f2ddc06bd52f8386f6d78af307a4104 +size 34630 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png index fa0b46361e..3264a61179 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9c17b8ca9966e4239e5c5fb9d9e5f0f061bd8d7fbf3bd9c76ce6473af537d05 -size 40962 +oid sha256:9a0a377b34502fb55b3ee72468533914de3c68df62531a6f40a41984c94d0047 +size 40996 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png index 0072d95bed..6d6a189589 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e73375d44fcd158c7cc2cd0f97b49c8a0f2d523772cab58778ed7b31a8a0d5e -size 38602 +oid sha256:433de908676d053d3728b8cfe03636d082691059dbe4c15b950483cfdc3e1ce8 +size 38634 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png index b8629728ea..0d1e17b957 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cb10d8ef6b7c6fd25e772428e03755f1c731197660ed7123b79c56629f74583 -size 33086 +oid sha256:56030e621f122f288b00dc642cf5ab4a3a09100cdcb92e068d8fc0f47f9a5328 +size 32421 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png index f3d5c2ec82..5c0a10cffc 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cef767ba47c98c5b693cfadef2ea0a260a35d3144c2dc26a3bb52bf632700d9c -size 44141 +oid sha256:61b6259baf820caca8eb8a28052142c56ee88a952148e204c25a53f709b87fe4 +size 44542 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png index 5c91aae383..599ce75592 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef46c819c931938a4fd856bb2fc697c964d4445d6cf9f8b5e2f341f22805061 -size 42001 +oid sha256:2f246482f43b0298e8948925fda23d9c306f56b484d42eeedbca1cce2dfb6bb2 +size 42382 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png index 214e107f12..224e1705c4 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de188fac87a59e6765360282655c081d7b93e7dfa47837b0c56dd77a9892ed03 -size 34616 +oid sha256:2ab51540f79f485e98b311210fc7d8b98ccb174b9e689587559878c245877f4a +size 38492 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png index 7f69976784..654f566376 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b53f00607190915bfb683c0bef1bd11f0804651fe6bf57bc9312afa20768a75 -size 42924 +oid sha256:b9962bf5c4f9870d51cd3eb3346bf0e2054c5f7e21111f57c7a7bb1938e81aab +size 43211 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png index 20dcda7f44..564035b900 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cd74bb51213ae932addc3dbaff2bd20d877a249c46ac5bb7656e03566189b38 -size 40748 +oid sha256:fbf5c303797332b7419e1529ddaa8d1f2f1cd6ed2086654880c7702fc3d62070 +size 40971 diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png index 82f91a65d5..3d21f69991 100644 --- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcad0014629c828846e05247a1ca0767c45906bc15378a00cb9d4e6cd7280dc1 -size 32380 +oid sha256:47fc45150924e2c1aacc86889824f7c0d7382af94ef420ff690871d75c7c3e23 +size 36212 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png index a5f2c81709..9d9b261e38 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba633616e0a212c5d527173132f1d53a8e9775b50e57a5a36890d9c718c6ec71 -size 37675 +oid sha256:0cf081ce4abb6cc0d5e80ac8387a91e29139f79762f107eb274d70246950c552 +size 37667 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png index 7eae95af52..3c34238b9a 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b50240738139691f82a0a202e31c19c23f9a6102b405c36349429868dbca3df -size 42037 +oid sha256:4e89a186de091f4e23630ad5fc71c0e01296876d8c3a2a5fc6fa6ac00220e468 +size 42083 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png index 6e9c334d4d..519167682a 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ba6d622adcaaccf8c6cfb8a446ef0d350a55d749dd6687e6a46d7b0564a2bb1 -size 37680 +oid sha256:4be298e13b622228c0890d8597b304507fd7c8057fb776cb8f1857a5b9e6d731 +size 37588 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png index 2008361428..43a70b4471 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.addroom_AddRoomToSpaceView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b028d11caea922f209a19d3802ba9c7ce753a3146d7d77461e85c741f35d6dc -size 42481 +oid sha256:bd2b7f829f6232c62980386225e82dd946e132dd50f8e571b74a7f305965d26f +size 42395 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png index bd95e60ce0..cf3ad87af2 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73608be44121b6125f14e3186ee533a5e479e30124e4276d342a25b476c00fb7 -size 46960 +oid sha256:5bf5bde86a7f7e873a6871d359f04f2139099724f9977601f5077b2590e97965 +size 46711 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png index 02756acf7b..a31ad79f82 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ea33f7c49c55c81f976111bf79bdee21eb58f3394c8135a0324f6a503dc6fad -size 47044 +oid sha256:078079bf6b37471c7a16363c9d49f7deb5bd9d044ed0591ab86ac8a20800ba03 +size 46795 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png index cf42cff8c7..5f77c0fa62 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed108284075a1fc2523ac38d63dbff3176dbeb97942995d4b83aa4f0bc3de240 -size 48636 +oid sha256:d36b97324b3794ff1d01fc13e9e59afb010130d6e022dcc2d7cc9387a13a787e +size 48387 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png index dc324f5b6f..5f67807828 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb31f13ac29f7ebbbe18d2314bfaccfd20f87c308edda618ba36a6f968e2fe33 -size 59929 +oid sha256:986cfe49b91c522cf3a85d0425c0677a6929c44dca1531923bff2c1766ce46ae +size 59583 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png index 462cc991f4..12d1c506ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ddc1a68ff028108928942a2ceb6df4caa3ce4322bea836b409e9a7ddf991194 -size 60567 +oid sha256:20742175a23c7116089cec7b9886ad567db50d899dd5dca4388534d6120d73ce +size 60252 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png index 34300310ab..490c357afb 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0322da022d57cbd3b167f939d00bffdba69758c6508256f1251826cf5621b18b -size 56970 +oid sha256:7228db8e07c808d0a8780ed06f04f22ff8f1d620327f9243b1dedd476cecb902 +size 56896 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png index b955245363..2eab6f67d5 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66b192f5717657395d5a5cbb9d5d409bdc8ed5df9afc010d5a619608a9e8b867 -size 32741 +oid sha256:9e74142d69c0223cfdb41d31321c997f9924a911b10961a85dec5d7a9b55f763 +size 32889 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png index 310ba5f663..71d817456f 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aecd8b25c698ed6c0b1682535657f196edd677bc0648ba0d42c1b109840fdf8 -size 33429 +oid sha256:335a04a4ba72df31e5daeede1a4e3a4a83c77c1317dc736bc29ba6722fc9ed86 +size 33573 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png index 386b14d431..826e4f3f05 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4d0e00d43b270c509920103f913609e784387b1c06e4cacf01dd106387b3ed1 -size 47806 +oid sha256:ee34f6503ca5de3a1f0e4bfb546605b3ab4fe2c710891733cbe16fb176cbef12 +size 47747 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png index f9e70f5c88..039776804a 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9b3bc80e39daf9809464a771089e431be993e8faba40431784990f61e7c2a4f -size 45233 +oid sha256:d28be1c00397b6ac456ef93dab7b1da469a7665380ab2c326124efeef3a1c150 +size 45143 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png index e6e4b0cec0..6847911e3c 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa6f0b045b83a4bd8b1f2ff9727964ec82ce7781f575ee6c1e306eeecd7da63b -size 45319 +oid sha256:81523eeba6cf9e4ea1e92cc819845bc95bbd76f9e977b462f2357fe1d2e3d335 +size 45230 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png index 3bd02250a2..763fae759e 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:471ed0a332d92a41a82536c5ac2204909391730b811d44f54d8b308fb03447f9 -size 46791 +oid sha256:cdff40a4ce4b77a98818798c2b6ce3ad18ee5117f40c47c075b30d7006626f10 +size 46703 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png index 15f3619b7d..760d8a7533 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:325ef5fb725ea61c382eec96668b4fbfca9eee08e1503eb72e298b4d62d684e1 -size 58256 +oid sha256:1d331297c0912ffc278828cd00f886c926a71e59aadbee9fef994258ea56c822 +size 57757 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png index 51d1ddc9f5..28ef695554 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd73b018c8fd459eab5849c942e4f992357abbb4d0e0ba1cc5847e0985ff335d -size 58836 +oid sha256:ee1ee7ae5a0350ca808b1839d7f033cb83947d33305a5cb5ff9a9faa4a5dadbf +size 58345 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png index daaaf7a44b..5f5b2315e6 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57cd29c2032d425f94551a0fda528ec5dbaa467900f6235fb40aa6009ee22bec -size 54897 +oid sha256:936cf07258d4a001b80b08df4b804c990b9e399bf8a5a544443beebf93caa298 +size 54825 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png index 28ae8f6a72..918bfffb69 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:053a96fd3fa0cc84d7c4eaa7464568ff89b6354b1c59a286a4f9a328f9744c8a -size 31849 +oid sha256:f3c87dfc20c6a6ff08e6828d47670812af0cea19640cccb9a9e5a8ca1f61728e +size 31957 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png index 3e9cdacb45..eadb187764 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb4de4018e0e43a9e2d858d14a36faaf9fa44208641fcd2779710e184d091ce9 -size 32371 +oid sha256:199acd884790b0eecd1333235561ad5dd2419c527e3245d58525d46ad7c51262 +size 32537 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png index 214301a8d4..926f10755d 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.root_SpaceView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa8f3988277d095b3747da35db43b8ae1c854bdeef484c243ca415d728610535 -size 45173 +oid sha256:66d5ac09ac603108821e0f333866a8e0473d562deceb103cabb67c4988be2c72 +size 45107 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png index 4a332a8477..95af0557b4 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6d6fb02ec741944efd1a9f994f096cb74cb43e90896934a815a7f3b0b75af06 -size 22085 +oid sha256:65e6adcb15b51201224b3566ec85b9a935a45bc5b9a4b9a5d2d1db125425fe59 +size 22063 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png index 4425a314c4..e50ea76ce9 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc1de1f9b7172a67649aff0c2fa2cfe2a63fbc46659a8ad2bcc5d417b3340e9b -size 18415 +oid sha256:f1b901895aa8cce2f8c183ed872c5b50575faf9c1cfb58e0225a22a08b0443ed +size 18385 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png index 00f43cff0a..83d98a46a0 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f26bbf2bbb816afe552d90b8acc3bce845fd7c51ace192693d0f92c5e5ea8985 -size 25939 +oid sha256:a87ef69a7143c6b637bf87fc82d7d04ea64d5024c6f44411d2fa60fb04d9d882 +size 26065 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png index 7ea1b72ecf..842ee7204f 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d8e3ab098ffd7d1e3e2abe71d0f6003915e470c68830c40617ca28f4af1e59a -size 26576 +oid sha256:7e6bb50005ca745dae15c2e4d7a5f28368effce2917c9e35ebeaf21b69e5c4c1 +size 26584 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png index f1d2fea72b..aa6160d8af 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8b74f059c7d27982d0f8d33e7b59a3a588d547f755d0bb352836fb55f57687c -size 21495 +oid sha256:087adc479cf8e9b87f4f269b94fb8b3661b0a6902a0b9d921622bfafa53e0610 +size 21516 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png index aa024acf89..8ea57ca08e 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f9c84fe76cb723586c05febe97fd45068de975494be4b0aebaf518b18826765 -size 17809 +oid sha256:afabb2f9b78af8ade56f79a4bf02039c6e4b3ccef8c60e89439de091da089ca7 +size 17840 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png index 2312f3fb08..c538831670 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9bcf5c92a8aaeb33c7d56709e8be38f6262a95b8bb07e4ff266685eab901b64 -size 25088 +oid sha256:0abf4b9725268a7762ff1b426cc97f96d8e71012d35102c968f518f05007241d +size 25210 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png index ae049d944a..5df4e31394 100644 --- a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be20525369f79cf4ebc5057f9cc46123bda55b923470e33712acab2f2a6841ac -size 25700 +oid sha256:9c5a119b8c5bd09a62382343043333dd478ab82fdb6f04ce420641edaf6fff7b +size 25649 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchMultipleUsersResultItem_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchMultipleUsersResultItem_en.png index cad7e55b37..fe1b0eaefc 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchMultipleUsersResultItem_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchMultipleUsersResultItem_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8af941ad8e5b553b82aad8b39abd8b4876d67b6906acf0ee8e4715673d8b35a -size 81462 +oid sha256:bd072c509f967d275a644f545dd262f3dd20f08137bf807d33417ecc84376734 +size 76496 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchSingleUserResultItem_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchSingleUserResultItem_en.png index b5e9457b1a..1b32cb5522 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchSingleUserResultItem_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_SearchSingleUserResultItem_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f776cecd5278761fe7904b63dd980804a6dc2d3e7acc37fb7ea71d6a1c1e63df -size 42505 +oid sha256:e2055eb028a88a3b657919f4831c42ae394d473be2fe16a62300add6c636ff9e +size 39914 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_5_en.png index 6a5952ed17..ab57e18c5e 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7624e76ce5c2f4af5b79f0be0beb3475d12bb24bb2db0cb1ed0efa506a5dfdfe -size 38153 +oid sha256:177b40d6be14bbf9c54722f8b6c0186b6b484a0435fcb43e888fe013a6f27a45 +size 38537 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_6_en.png index 69f773c50e..757ee11ff6 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94adb63a8654181481e7f62652100cf0294f9c79c1555e2e078b1e27bb15f066 -size 52548 +oid sha256:998781e9e468edba5b1c5bfe6bed83c8c0dc49754d5bc965181a061abf031b94 +size 52304 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_9_en.png index 850752a55e..b4545ab901 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a75b5ecd5a4747566d8a39ecc4cbbc3583e5abe6a78f9eb820ae0a8d7ee9e7ae -size 37654 +oid sha256:32e7c3787719824d7921145dbe734db4ee4c733c81e7e236a30b0cea9c0c4867 +size 37675 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_5_en.png index 80c43282b5..459ade084a 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba3e35a4ffc2bee9cb71dd997b9b431faec269a345d2e3f7e93357aa2f16ef2b -size 38941 +oid sha256:bd727247ea40290f61c8ab9a9a1a4397985ebcfc77e31fc4b8efa8741cd671ae +size 39343 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_6_en.png index 90c20a973f..553160b7a3 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e908ed57e0b9b8072267b571b7ef0689f643ee30e40d26235184cd4bddde9be -size 54034 +oid sha256:3bde94f2127ee00f5a6403f8fa246658269c59eb80542cb978d6a62928566eda +size 53675 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_9_en.png index ca5a3f8dcf..66aa1970ab 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.components_UserListView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0d318e399e0b3120ad74b409f0d53197c527f2d0e6d8396e301b7a70a9473af -size 37592 +oid sha256:28f70212c98536a1887e5c2d3189ba080bc51528f433667789767562584c1006 +size 37493 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_0_en.png index ad21975ebd..a95c48fec0 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2c745de02098d9e38f75fa7c34f436ad4f7770f4d0cbab03949e0f3c6bfc958 -size 25557 +oid sha256:1d22817145519d17475a09dbe9f5e3a71ec6b5ff9e917d7a92b06feb3ba865c9 +size 29049 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_1_en.png index a2f44ea5a0..33fa7369ed 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b094f8d20b4b946abcd95bbb0c1d64a0190a5c651b6f434dbd9aef2857a315a7 -size 19991 +oid sha256:27249bb1a928cb325557c8aebf6de344b7cd7a16d3420e8711591f20a3eb36b1 +size 18947 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_2_en.png index 5e3737d938..ac26997f26 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3fde1e89269c46ac8a6b4f0a05aad45bcc806f2a1b6108c09f7078209f7799c -size 26574 +oid sha256:a5f0abeb2bfbae8fc6b864ea2e6a90b4ab3bcfd3b9f5fa4a06c6a314bd194a5f +size 25660 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_3_en.png index 815a4f9ed5..d0d1748a80 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76947cf0ea5358646af7726987bd3cd3c49694aceba9850bb0e60a63f3fe669d -size 49542 +oid sha256:2f2bf708bff2bf2a2581c0eb0adfa63b38f0fd5f5fda214594049c2b40a98e09 +size 48731 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_4_en.png index 920ac350fe..51f05258df 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf3add28f315b5f7de06dbdf5b6d5fb96552f648a95a620e50b65fa970de32dd -size 41513 +oid sha256:4b372f9fd8570c977cd882f396004fe8fd0794ca3070a11394e0d6af6354987a +size 38154 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_5_en.png deleted file mode 100644 index a95c48fec0..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Day_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d22817145519d17475a09dbe9f5e3a71ec6b5ff9e917d7a92b06feb3ba865c9 -size 29049 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_0_en.png index 8071ebd4ea..8e03ad6ce9 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4c1e564d2cdb6f6a61a02274d63e2610c9b75f7f53c36b473716bba2400798e -size 24752 +oid sha256:bb277b825fefc001450c45a0786afce546f2e0cf7be878ed7bcf401c501c2431 +size 27988 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_1_en.png index 5e457ea444..b37d6fa5d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f37ae64424323d6772fca7b43f175e8c02356833bed02bb9661acbfb102c194 -size 19358 +oid sha256:c5e6a562525d8ded0fddb365beaef933d2701bcd4548e589bffd54e1bd0a3b11 +size 18087 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_2_en.png index 6fe0a0be87..4592dbb299 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d42e8795792b250696206f5f9fabcac3b7249b3709cf90d8f2503091769e5343 -size 25324 +oid sha256:e22f1f07193011c1db785e3827e73d05d8bf96fd12559e3d7a744b2b978b7bf4 +size 24196 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_3_en.png index ef1431db80..7aea3012b7 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:884e301844c610497bf5b5d52445e8124e9eaafd591853a55ab72175096942ab -size 49336 +oid sha256:de50204e7681fead39e7a8f298149b2657a3663197ddc1cfacce4389641f74f5 +size 48222 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_4_en.png index 6b225ffae0..76810b5897 100644 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81be06688e135d746d30140e61aa168f0d7bfecad1a12e54d9d26e3eb55be0be -size 39897 +oid sha256:a8a1a8fe51f741748817a0a1c497fc164d0e1e711ce458182a654b1fee8e5741 +size 36417 diff --git a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_5_en.png deleted file mode 100644 index 8e03ad6ce9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.startchat.impl.root_StartChatView_Night_5_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb277b825fefc001450c45a0786afce546f2e0cf7be878ed7bcf401c501c2431 -size 27988 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png index 7951440a2f..3419a50b07 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8185065602221f9bf675398d42c16f9e0c7ae65952b865689263878f43c545d -size 24110 +oid sha256:9973d9a9fa00d362eae8f50c2936e5ae0adc2653b4671413b856c1e73538eb73 +size 24261 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png index 12563b1b76..7e6ca5d507 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c3ff3444b2f720b09de118af05e1376001bf3cfe9be66acec4a68cecfe9ae6b -size 22357 +oid sha256:4a4154169fd6e74358001ecc151a0cb4635423bf4a0b2d72f1d5bb12dfb0ea95 +size 22446 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_8_en.png index d0d02d6321..aef669fae0 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65446447e07a1a6cc08ed566e1aaea7a1209f1e1f5f752dec5e598819333761d -size 34595 +oid sha256:781bf8c86e88c9ee564f45de7446ee2eb554efe68ae9e708aa1b5bde7fce6d6b +size 34137 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png index 61d82d6084..224210123e 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2f2787f0aacfa36596b9fa0e25d990710870bf5ed25d831b47c5c71ac1d3de9 -size 23377 +oid sha256:e28754f90d584c329a5e3c51eba89c166320c69fda309b6b06b6f5ee9c74c1b2 +size 23549 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png index 90376bc952..408be83664 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e7e43be60d1a545621d9d64ae3500980056f2c405c8d22629ab63e149aad224 -size 21816 +oid sha256:3b815d4cf8ed95cb983072d75419637c10bd4808ddfbf14865409d0a4fd64c37 +size 22070 diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_8_en.png index 844a7c3c54..3c90a4f7ef 100644 --- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e467732ff44f080bb1a698d4f6f2427480c5456980066ffd9ab6970b093c3d64 -size 33206 +oid sha256:45c706bb63ab7df8e66898ec67fabbf8c227cac421b8c5eda84a2f6532c8f199 +size 32504 diff --git a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png index 8d3858b48d..852737ca38 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b44e998e6777d291d1ece8dd0417b585627ad546376e0fc79fa6854d0ca55a8 -size 43011 +oid sha256:8c2e04e52d03cba2460f7a0799cda970bcc49bd45ed2cb2de3d39912c0af3a2d +size 42267 diff --git a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png index 84cd91fc57..978e8ae5c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.accountselect.impl_AccountSelectView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a49586145ab8dc1a11ff0212e3a38d865d39c929ec9c63083c9e2ed0e50ea6c -size 43869 +oid sha256:db7080fcb2bcba0ae22264ff0cd0cc5a843b3d860d0809750d4927a0eb1a9a27 +size 43353 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png index be83c1a4a5..ed8f933e05 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar.internal_AvatarCluster_Avatars_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ed3c503d7aebfa335e7fd32e3b324acd6554d4bc64c4b943c20e2c9b01749a6 -size 59593 +oid sha256:117c38143692e2b556300ec2e004d778c7a0ebba898f835f6c42804ce51128f5 +size 59248 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatarsRtl_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatarsRtl_Avatars_en.png deleted file mode 100644 index b96321378c..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatarsRtl_Avatars_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9dda468dafd8eb072ad7f3b731e55fbd1f8a4e11e8765f566b929b1d367fd0e0 -size 13693 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatars_Avatars_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatars_Avatars_en.png deleted file mode 100644 index 097602787d..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_DmAvatars_Avatars_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:69e9858a04efe0160908bd0b85990dce8e983db33408ca58752bd4dc1eef6861 -size 13576 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Day_0_en.png index f4959053a0..a14d3dcd4f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93573679718ae2a1f3a35474252405d7c6dfac545291f9ccd7a722ad4507aa73 -size 58491 +oid sha256:eccf518dddb9b946e435bf391e56d6c4ddb4b5a7784d5bb37255f6e41e115376 +size 58509 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Night_0_en.png index ee5b8d78b9..e93b92458e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_SuperButton_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f50b0f7cd8c1e3ea5ac706f93d4dfc5854fb56004d14ecf687c99aaa06c79daa -size 56908 +oid sha256:a6f1cb3bf4371adeaba17975c9f3d601bd021a29f4ec2c2fd9517d1a35fc61f9 +size 56982 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png index 071b8e84ff..29fe0beee1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fb6f62b05652561db4287ae9da33364dbd03d80f377674539055958e098f6bf -size 24840 +oid sha256:3f56ec8ce787f1c5aa7f107daf76b1c114b4336b9e863b09392966a1568077a3 +size 24714 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDivider_Preferences_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDivider_Preferences_en.png index 31b672c5dc..c57939a437 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDivider_Preferences_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferenceDivider_Preferences_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c85b1425ad016cf91918b845ae26b0d2867d8ee87ef2c234720d69350083232f +oid sha256:4a1517ac6b95b00e5242bd15cf8cfefcb996a5bd16f1e464be852eceecb0c006 size 3723 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png index 6e0970e7b2..64086bcf0d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7dd7f2e6cc58bdc6e3e0b837a95c71b6349acc7596316c13cc3ec03d302a3583 -size 22143 +oid sha256:2765082210b5e2d66badac2176460337972df0c5c82c8512a700fe941cdeeb7d +size 22232 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png index b231fab335..4b3ca0b6ec 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.preferences_PreferencePage_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce795382500675c5f7b6aedea6c02353f91115e807fc3f6b3f8d9687dd69b74e -size 21501 +oid sha256:9020479ebc44a6a41f4542bd6bc78240ed701dde274a320f2dabb5e3a62d8fe8 +size 21548 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png index 9a43a2f7c3..c2cc051da5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15974f532df8a3bebbedabf337889e3a5d71cf9d594389e00dd3e7ce62fc84f8 -size 30491 +oid sha256:6d15e20d75dbdcee96ac1fcaf1f32113a70f4a6764513cd43cfe107c435448a1 +size 30474 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png index a31ba55583..db7e037964 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46f03028f5a3275c6b5a988de5dcae722effb383830f85fada87384ec6f5f1cb -size 30854 +oid sha256:c807742a58a19dcc4d40ee9354a65bc4078f861fa1b6ad269363d22f10784cf9 +size 30875 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 79992a9708..fa2208b50f 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:30b39281fd597089c94c8ff1680297143989d6da97d6f5b15198be5f92469483 -size 114653 +oid sha256:de697fcf50bd244978da1143a80f872650a424c3e308fd2c4c66005f30cbf6c5 +size 118141 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png index 7a670bffea..5a9428a52e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_DropdownMenuItem_Menus_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b46d08f9eff6321a3102cc9756b53a665702a32021365a2d2b8917446beb3efb -size 23804 +oid sha256:71076c2b51e510c0a7bdb8d2e57196bdab02072964e82804fefd853b3eaabfc7 +size 23957 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalDivider_Dividers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalDivider_Dividers_en.png index b391c4321f..0cbd7da153 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalDivider_Dividers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalDivider_Dividers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c8372627f3897fb6caaa4a23fac112b4e2aa7a9d6fc8a6fe6a48752c5d06345 -size 3845 +oid sha256:57a530a13f053a775520527c3909e96aeb599521b42eff587061911a3e29ade5 +size 3851 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png index ee5c582240..58546b8522 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75de341f705bb22955f9399180519b65ad59c2e1917317d56e50ea4db78f35bc -size 12102 +oid sha256:30357bf18792db5ab20085010251170a724ffe8c00313facd04ee704e806af64 +size 10982 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png index fedead0fa8..52fd9b9693 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4811667dec0f32accc5e2c8a5dd5c0b34017fcff8d1e0376ee7a4708426bca0f -size 9838 +oid sha256:58b9491aaa5554b89d86c62a5c9c44975367538c6baa5562f8dc5768f76d8264 +size 8705 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDescriptionAndDivider_List_section_header_with_description_and_divider_List_sections_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDescriptionAndDivider_List_section_header_with_description_and_divider_List_sections_en.png index 24654229ff..4b6b96dbe3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDescriptionAndDivider_List_section_header_with_description_and_divider_List_sections_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDescriptionAndDivider_List_section_header_with_description_and_divider_List_sections_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56e81753c623450107b1f34eed6ef12473c3891c5d6f23e90fa1265369623b35 -size 25341 +oid sha256:3145a6b1b6b74831f0aea71fb002fa7fab97affa6f8bd9a2a62a22bbf4dd391f +size 25114 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDivider_List_section_header_with_divider_List_sections_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDivider_List_section_header_with_divider_List_sections_en.png index 88193307f2..ee77d862b5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDivider_List_section_header_with_divider_List_sections_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_ListSectionHeaderWithDivider_List_section_header_with_divider_List_sections_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5abf26afbad167c6cc1f92a60b1eb73c0d2e1e5c1f354d66326990c7a729ee9a -size 8610 +oid sha256:aea3fb4fabd829e27acda01561ee58dd885c8348d150ce05d128081e7f9f3517 +size 8582 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png index f5d8e0173e..362e750080 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:368aec8db00c98f5e2b73763826f484c4ca395bfee9156cc7796029f57ceb907 -size 13464 +oid sha256:7a804cd9678ae0aa149065dfc7ce98bacb30956ac75a17f80a7f32acbbb6681c +size 13598 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png index e16de72924..1aaf062713 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:707eaf1db064e542041746c1fff6f81a47e95a45579a9738f7f02573ce6db0f8 -size 12261 +oid sha256:44fa03d304319f972c0e9c6e297e4b86bd0603a90945126f47f657d566241911 +size 12500 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en.png index c4375fefcb..419371ac1f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:806e56c218a0ad1c69dd997089cc8362e30624377fdceffeefadc523c712203b -size 173137 +oid sha256:90218efcddebe9896e2b2c937f4b408ffe07479f2ed5418ae3e070b718195cb7 +size 172920 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en.png index f946b46642..e1ec5c888f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerViewRtl_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e25f6630680dcdf1e685235fc551dd6ea853b0902f30234e5fb9d4050f584450 -size 169862 +oid sha256:22523ffcdecb0d1775ca08f3b2fd29c24eae32b7dbe2857dce5d70020c975101 +size 169627 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Day_0_en.png index 7cea979d00..1d03fc7475 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8185dee1ad113c2b32a74de58c440abb366337accd1dea12047a85d323eff9eb -size 171610 +oid sha256:39f937c61446d35ddc45f76aa53e612ac3855d00cbae475574c954926cda8250 +size 171084 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Night_0_en.png index 7118f2310c..9ef00c806a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AvatarPickerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a13dd8a9228c1526482f0c7fd5e0347c8fa4998849507b759a0c9548455adfe7 -size 167957 +oid sha256:f0cf40f1bb4d180e2cebeac0381cc38c714be19b57f9b0ea7d0e316a4ead531e +size 167332 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableResolvedUserRow_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableResolvedUserRow_en.png index 3aa2f3157d..32ed8ebc5e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableResolvedUserRow_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableResolvedUserRow_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a986526c80130e11a0ec9c6450e4cd77dc40ef4a09082a1c526eebabebf4c03 -size 49746 +oid sha256:ef82fb23d7da19f1c3f37c3fbd1472f138a8edb27c307072cebb3b2444c239ae +size 46245 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableUnresolvedUserRow_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableUnresolvedUserRow_en.png index 1ef0d7dd02..4655e07a84 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableUnresolvedUserRow_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CheckableUnresolvedUserRow_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2ca690a89bebaa351990cab34155afcfa7a0b9f02312757fcb933ba97ab9310 -size 101952 +oid sha256:24c890859cac9e93ac69ea6914902a185109e6c063c1187f64e3740307414e3a +size 94973 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png index c7e3599c58..1c57c30014 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5 -size 26077 +oid sha256:fbbe069eaeff3c1bfb39e4b3e3356e92864f10a57ca4ccb2029e38b5977b6f45 +size 25648 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 d7ff4a1d2f..e8d101b4b7 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:26bf76ccdb56d042422553f557d91d0f26d874a710f696ac106c5c2b5590d332 -size 38833 +oid sha256:97e6bbc6cc6d5bb90606bc7f934258c8060d7ca5fade8da6a1aae288e8558bb4 +size 38390 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png index fe44b8941c..0a5082edbe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_CreateDmConfirmationBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41 -size 25232 +oid sha256:2b14f8a5a50deb5dd4854665cd41511228f5ba47b0719bf35b4d1675d0f20ce1 +size 24527 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 f5ff7856b2..6d872bf044 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:8413aed02383572cfe8c481c6ba8b0db4cfb3402334c37f2d8b54a73fe4bf594 -size 37343 +oid sha256:841042eabcd85626a5182108edae46b4514f84a8f22c57e324c02430536acff2 +size 36738 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_0_en.png index c6e079dee2..03d5ad5880 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0805bac4bf9e1c5bb16b4f81b004bcc952563eef98101d8c9c6e856a414977d0 -size 11219 +oid sha256:0dfea70e781debf7a293aee2967c3da314101020698fee97d3551e25f5f378d0 +size 10655 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_1_en.png index 74bc9743d7..3a1ad156a2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b61c5a72e8e63a1775d20717c78d8e68e46c7c22b8e4ab8c24154dc3d48d7d0e -size 11428 +oid sha256:33482ad6e5033800e911dab44d4ba5bec15cfa58749102c5da4e2c57a8e760e8 +size 9783 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_0_en.png index f21d99fffc..27f51b2778 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070163694fe10b465f2903ce2cc88f9ffc67d9489aa5a4e204cd087fd02b8642 -size 11281 +oid sha256:62e127900d00756ecc806eb9d8efb6a11cd9cdcecae6b4bab643b7306a06be2b +size 10435 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_1_en.png index 9413b91e88..81d4ceda60 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserHeader_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da4e1512cd7a58ede774755b6e3ac427e90c437bbbadaea8479506af776999c3 -size 11323 +oid sha256:e39bc143f7ee45c47ff57bfb53a773d7df2ba14568a9368643d8bab9dbbf4955 +size 9675 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_0_en.png index 78e3e7db33..93e17f26b8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aa5b37b57d1ef1f219e0c5213eab3eba3222bd0453988a5c708e15ba89d8fd4 -size 9772 +oid sha256:4c37d48eb1fde1eb67cb3130505414c97486d1b40b49be9a6c0770b0ff9e50da +size 9277 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_1_en.png index 10d4beffb9..5174fbe9bf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a992addeb8525c7df4be04514c82e22c8c1269d7c14aa25497efaac50e844a8 -size 9410 +oid sha256:00a060c5b66ee6febd8bfe145548aa3b2fa43ed0852e4224ab08a4641cfdc6b5 +size 8075 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_0_en.png index af7347092d..d55e71d415 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e850f65631de21c8cb44965f66e8ffbf0b1eecf2ed6f91e6669f0dc89ec0e4b3 -size 9784 +oid sha256:4e283d48e8eb569f44576c158096ef0672146392411e2e768350ebaff9f71ee7 +size 9073 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_1_en.png index 38dc8e36ba..fa48ce852e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_MatrixUserRow_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07b7d5515f0ca6e6634e8ff4b53c73f48b1170fe8d2488c7a8863d1568c52cdf -size 9420 +oid sha256:d23f245942d6353707c4252a05935b9d59a5a8ef380f351d0341b804413969ca +size 8101 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Day_0_en.png index 13b54d61f4..6fc398e5ba 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca4b2ddcebb957944522673d20aafb967ef118ac1a8564f33a0c8745f0f7caf5 -size 6268 +oid sha256:2a6797d1d45c1ef189dfffec805604b06dd7820976c834233a99fc803721b663 +size 6500 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Night_0_en.png index 30475df2c4..b599a59788 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserCannotRemove_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d56cc64425872a2dc1c12ab7d2e0d9fe43e89586e6f5f1805684350281baa6b8 -size 6744 +oid sha256:acc4553e995907ecfaaf74c4e7bfdd9fd8ccf5cd6ba8396b25c110211c21975e +size 6614 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Day_0_en.png index 81b127dc0b..1d334393b1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:160abf1659329b6ddb61e9f3bbe9cc5cf4630242429093a8d2d579e0b6efd78d -size 7687 +oid sha256:2f93df7decca6ced4efe9f739d6f877b2411be874c0cf011a146a66e0ee2fb69 +size 7790 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Night_0_en.png index 7ad44c9fd2..efe44c5f81 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUserRtl_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39817cc9c0bcb7ae058201c8bb9e7f68529142161019a23296087cd952667473 -size 8143 +oid sha256:7a52a67692a0fefcb499af2f3e0670b663b1df657ecc7b61e58773db9ea6615f +size 8057 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Day_0_en.png index 08ebefd43b..230de5c2ef 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40efbe8c661131800679fcdf18870d05cbd24a7f62c7c1adb998f28cef3896ea -size 7698 +oid sha256:b095f97e5ac7279fba15f7ea8dc1658c89e28e4f72b8f93b7c32a7b6d5bc4d23 +size 7807 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Night_0_en.png index b7d94eec19..d45e2d4b35 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SelectedUser_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:774bf98c634fef1339d95e8eaf5f5219535766f41f2ee8b4c124107f381bf052 -size 8167 +oid sha256:5a0960b2353e902b04682818cf598fad7ff4ccee9e9fe7aec13ad556653b702c +size 8091 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png index 25dae1f86b..82f7e36af6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ad5934da41af5f5f350adc3d5d754f1ed16e90546cb20a0874e3d069010167d -size 10117 +oid sha256:aa985c9b0211cb12e617e8da73669777cf3a5ec5b77b79f232ccdc6e0cf55373 +size 11760 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en.png index 491523e822..e38dfee86a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81de93c5c71bc1552dc93343bae9168ce35e0d4d4430a47311ff80b9cec6c3b6 -size 10633 +oid sha256:71bf90e818949065c7620bfa8e7148a8f9be295775e1b29cb251d2a00bd87853 +size 12173 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png index 7201e63c00..e3c16b4b40 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ec4038e9d1cd25d8ee27c54e10f9f3627bf6f8f4c83201b1bbc3fd73641f1d5 -size 10333 +oid sha256:68e990772361fd240fb20b664731613b5a2f187499f2db48e130673f16acb33b +size 11575 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en.png index 308066382b..f4bcc684a7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_SpaceRoomItemView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2a837de06ec9369c5a39629579c237b4f394a586a2ce85a5c5e15d444c98314 -size 10323 +oid sha256:05e2b62d4484990cdd837ff233a654cd6aa8d82d1c3965351bba86bcfcc1e1d0 +size 11776 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnresolvedUserRow_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnresolvedUserRow_en.png index 1e4ce1ae37..5d59b52717 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnresolvedUserRow_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_UnresolvedUserRow_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86d4aaa9a2ad499be90ed3a0d9a15fcd23cba4b40395aee53028168717c1fe9f -size 54739 +oid sha256:8a2d52c8f8903881357cd0590d5c1bfb85997068eded3813aad1bec4f31d853a +size 52452 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png index 8a527331d9..2e05d081bf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67be1e8e93d0c10366f4b37f14ca0f90c1f4ccd5da9024c9a396d3d46507e18c -size 40339 +oid sha256:64f672ee115fee2fcbf9a276926fe5c2c97820f2644c4f37e2492fa1de9680a5 +size 40454 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png index 8a527331d9..9a1cd37301 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67be1e8e93d0c10366f4b37f14ca0f90c1f4ccd5da9024c9a396d3d46507e18c -size 40339 +oid sha256:2ac2d77c7d905175cf7b49a8fcb4aeeef12fa40037ff8d1ff8210ffb876e066e +size 38434 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png index d53b0ecf7e..6de4609ee7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f89a1039bc2d101af319c0b6259d8eebc9d313ae34947aedee525945be23799 -size 44604 +oid sha256:ff64b6429ce00980b65cd4867bc524bcc4967be033019a5f6a731f1c17140650 +size 44786 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png index 69da701004..ca2d61a8ee 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6144f221b9d11c70d15b54321bfbff3d1de1454e6c73be34d7b2e82bd1625a94 -size 30701 +oid sha256:a335f858bbca40a0bdb3bd239fdc3625975d44ddc45bb653815f20acde9ffd0f +size 30710 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png new file mode 100644 index 0000000000..412b11e2d2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e11cd3b0b82727388e96651b0d811911c38210eda52eccf1f9f7a98730e716b5 +size 40906 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png index e16a2bc691..4c1abaa95a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a512016e96b933f6089e60b634fa77f140dfa7de50d3a8c1a62092ad552d7f2d -size 39216 +oid sha256:535483df39fa09e9156d390867308a686c0bfb74b9eb2e5c2bb686b641585a9f +size 39533 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png index e16a2bc691..58fe1e398d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a512016e96b933f6089e60b634fa77f140dfa7de50d3a8c1a62092ad552d7f2d -size 39216 +oid sha256:a90c7beaea159f4d82d4ccd5aa8dd2187c7cf23d8d4c8e9d6dea817023dab80a +size 37130 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png index e36f282ba2..304ecbbc30 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8608e63ef9ff79fd698c202d60a8c73f8874f8610601a0ff6dc85663ed82c7da -size 43537 +oid sha256:386e8650f2203d6388377ab988d2da7a9b052b96a6a2ba5d89d3972a70f24ce3 +size 43805 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png index 2534bd5d0c..c4af30aa2a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2898f00a828a69ce9cdd56994d7878f6aae8c5b2ea5d1150df57aa2ebd7e537b -size 29233 +oid sha256:423e92ee408ca6c3ab7c1071245810fcbc2d25a7ce62b3068f1616c933ad008c +size 29216 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png new file mode 100644 index 0000000000..0d8d5f099d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8085e4ff5c8361629a3b8559fc2a5ce417b80fb3c3a347f7c3cef049c123b5fe +size 39998 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png index 48c6bff9ca..d9f1bfbc09 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d81773c99f740992755d35fdeec1720267856c0b201f246cdb4d8442ca4e3063 -size 8301 +oid sha256:de447e1f21d51bd3e21a0fe4de6817c375a408beb8225a6ad5deb85025801e4f +size 8293 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png index ab5e7492b0..ca71909e7f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ee522d0d8f87577177bbb2e73382d9736178214c4834508f2e7bbccff6ebd9c -size 13049 +oid sha256:f6c7e6ecb02d33e3849ebd2624dfd1e994d77b9f7f43e7ffe9afcf33e8436d44 +size 13041 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png index 421815294d..037395a7f8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69e9035843c450cd0fb45ed0e287ad6ff43fd58c12d6f27687e5f19527079563 -size 35318 +oid sha256:75f5d30bc99d5b09a69b77d544aaafaf665b1c377405f7ccb04092ac0da0a114 +size 35334 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png index 3a0c862132..93cbbae2f8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1ae3ee5e7bddb2efe646024f04e051c37d46b24b12f75d861b0ed585ebe8355 -size 8078 +oid sha256:7b3105694519d42956dc164a04b7642dfff4a303d7b96cfd2967ea3ad1bc19cd +size 8071 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png index b902ce98ab..6c89e912be 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8711ab77a43fb6ef26d7ae79bbfbd3d49ae11f4de9d444bfb8beca3b44dd0863 -size 12450 +oid sha256:8fd9f5dfc87d0a6e65e9c10c67f4359abde2ce85265770f8a47e3329f2f64a2d +size 12443 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png index 68da14886e..1fb003e7b3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d2e1ab9787c7168f6e7f6ebed884e1dcfe45f2304acb241c2fb0d63abc48468 -size 34388 +oid sha256:97f49b6969c5a852624558077f9fc9b0add39a12e76e744a385c85785d4489ec +size 34405 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png index 0b6cd8eb47..ca864249bd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b645f3d06de7fed5673ac1b93df45a9e60ac9cb0829a132e413df9188d01cc38 -size 8937 +oid sha256:31283735c33057ede4d2ba20024b00ae4a4f45510cfbb47c21f980f7151c3858 +size 8931 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png index 66625704c3..e4eb34aaac 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4019a4116933dc5fe984eb9be639e8a4196df05ad154a6a2ffc7dce4e3862ef -size 13341 +oid sha256:0a2decb05c73655390dd2cfb3d88d209b17ded46d5d6c40cc8672e5781b48348 +size 13334 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png index 5cb7302aa2..d2199b862f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a00cb76be6037667a3c43f708dba216275075c2b835d42e2063320c6267b18b9 -size 35952 +oid sha256:21803c66fce41f53cd7796321fb9f3fc774e0c4fc99cd3683a3a8b22e00e2615 +size 35966 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png index ef2fb3f45d..c9a00ed743 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59b12c104b96d9c53d9d2ae4f369e3de2217776939f4dd83092e9488f03c050a -size 8641 +oid sha256:11300a4c85f2e8daa06ddb4a3f8d6f72432dc581dca279366b1229450d3b973c +size 8634 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png index 78030e568c..e1090265d0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f92b128f85e00172b208f8bf0aa03c1160be64771945f488f5754ea0f0c15816 -size 12716 +oid sha256:1c8a1759a613527cfe5912f6e811121e29611ed05b8a8cecb95391ffb1bc5be2 +size 12710 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png index 19f3a00321..e73cf16f0c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b9a57364c3cd404b3564c88b802d73e98f6d9f50728e16ab07a121a658b8d3c -size 34927 +oid sha256:c53798a077d020700ae58aa93aa507dd7ce88984c48aaabf11ba863e8b1773ed +size 34944 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png index 3d0bbf58eb..ecb8160d12 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5b5c9b528a7b425b9181c2ee94024d09bb4c4477f7fe54e1d77c0a08c0d2c48 -size 9287 +oid sha256:2ef97e6e11a66c49ed5ba1863567f24097fe8121ea34f7de5fc5cdda3f0c7a0b +size 9279 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png index 7dc0750153..2ee1097de9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c3fd4ccd645308d061d8315c10a1c1da70a0e33032a50c40d3efe1b558cc9f3 -size 9839 +oid sha256:49bec9adfef9cf1637c722f5c96e0fec677c21a4ea3f89fd4555a9c894090be0 +size 9833 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png index 603298db3b..793b579aab 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac9e0112568d7067b7c439d423c6c282ca35889a64fbe34d6f778e935ada9135 -size 9585 +oid sha256:a010f16ec1221a43ba2bd5266da05f3de48d354b98dfc087974ff605d32d095d +size 9576 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png index b2012fab3b..08a42124fa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70dbcc38e6353a77decb7eff90e0b2ee06b2d9f3c92a688efe09da308e5ca1dd -size 9723 +oid sha256:c91316c96e75ba29e3d1bae38cb8b0abf255d7316ff3a616fdbddafc550c744f +size 9715 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png index e147449055..7250c6af6e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ccaf6378995747cb914a89da7c4a74a60b43e3dd5e4e4ec4174069b0ab8d814 -size 9828 +oid sha256:0f8945f66443988d9b36084868241079e22e61605f21cad055d8e21e8e063c0c +size 9819 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png index 53d0a71144..f6828af0c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e599734b376ce6c8fe4989903288e2e26c0df2fd27c4f20afe9c5a2f8f33dfa0 -size 8719 +oid sha256:2cc4cb7aca28fba19e6e5d9398ca7d26c9c361298137552789024f84a78a7f70 +size 8713 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png index bb8713cf6e..2639fb33e1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87d86e984ca7238676d5e642b1706894b91f6f12fc19ef5f0545ce6ad34342e2 -size 9344 +oid sha256:48a1a38461c9d2f0bb66878b977673531808eafc0aba53f17beff779a575707e +size 9337 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png index aff2001f4a..a68f3fd1af 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb88847548a4c520fe5fa7235161c2e437faf59ade57405447059f3f12590468 -size 9101 +oid sha256:98990600b7b05b8fe64404ef1f463bf5be99f48b90b6d6aab64f799d5b03f737 +size 9095 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png index 77fcada22d..de56ab0f99 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3030f28baeae07c89e784624fd58fad8cb4441deb669fa91d94f847e566700d -size 9200 +oid sha256:c662180e91e10974f7709364a07fa54194df0061425565e08644e731f5922dfa +size 9196 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png index b3cbcc11bb..fc138d7153 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3fc0c77a58255fa4d5e5c45d3baeeeed7bed667c5d2b1df025b8805b9fd2b5c -size 9318 +oid sha256:5c08ae126aab643824bd431aa4449a855e382e65840ebd980d4849e19ed17152 +size 9310 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png index e2cddc5835..eb40570d60 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aafa8d9dc782dd8829e3cc542a0247173b2b1f7f13f0d2083ac276b88e7708e0 -size 9348 +oid sha256:9a3c0e76d07f250735fd75061fb076732180b254d8e8bc145887e0d307272de0 +size 9340 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png index f5f65eb855..508eb5b48e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d49c7282b5ec080bafca160e1f257e812ff09ed18852be159dfccc7fc29a5cd5 -size 11663 +oid sha256:08357309dca3a8a3cd8a38f0928522eeefd3a0cdd91990fcc590929e187b162a +size 11676 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png index d7df5420c1..3f7c6715b7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae27b73bf52133e75c0685dc584f46c26cddccdf864b01bb242be41bfa589447 -size 36842 +oid sha256:d03f232373aa8cb3f0f6fceeecbd4b1ef86f8703ac6b887683cbfa5c5e5af4e4 +size 36831 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png index a6ba59d4d3..2cc97641ec 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26303598b3bdf1a7d977f8d54e9a36810a957d849d4662cd314df5a0f862ce85 -size 7642 +oid sha256:886dff267c22e2ba300b81f13e3071756483c5938e9d1cfb357f633357f5ff06 +size 7634 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png index 5982076411..b83e6823b5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bc8b9d98e0fc0a2ec522c74988e9461e1e5598dd232362d209c7d8c10fd008f -size 8785 +oid sha256:5c11e7b79420af53a8b934b3ad6dbe9c68d8ea5635322389663b55f62160eade +size 8775 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png index d87fa0e77d..a0ee911211 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25a37d6659117fc81de7e7131c503089d9091ab25fa408ed4305fda13c926014 -size 10917 +oid sha256:22f76d8fbeaca8d0b986b4ac17a28c16e5f757e2022466200215fca333f69ede +size 10929 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png index 5fdc8077a7..6f77d72e3f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cede06219d43d773b4e1ea81c7c81ee9c28c6e6d002687fd3133f9d7df4b9c0 -size 35125 +oid sha256:73cc7f1b0f8b9bff2b2e6c9a88b833baaeb9f39923c2113a006399c6a877634a +size 35118 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png index bc1bf1c7ad..c82f55de11 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f924b3e7b7471c973719e2d33c3bd10f9bb63a4903df4e9ffcb5c9c1bd7e9ee -size 7249 +oid sha256:d4d538dd90a36eefd2f9c8f8f66847cce6bc17f60e503513bf1890e6b5403d12 +size 7244 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png index aa15a1f8f4..be668fb03c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:91da9a2b01eccb0451288e153d01d159fe80e1b0b2c5d366d74622af564ecd33 -size 33157 +oid sha256:691c6ffd759b01f216128c1f7d59e7cae76e1f1a83c95a8b89e6f2f55dce3b9c +size 33009 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png index 809459297c..968c84c321 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ddbddbf5303bb86f901cdd880ca2fde15c7c4af22dd7cf1a7903dcab5df25b3 -size 40254 +oid sha256:f6bc140ee95f9971e95b1eebdc08cd524310cb9f19b5f392832a9cac42de87ab +size 40369 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png index c3f0453d99..341dc6115f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d60dd5ae119c4c020177fe37f54a9a2c8f05ee1b9133d3f04c5eca125bb1f43b -size 31703 +oid sha256:e218343a909a15b66c6c00f46c6bbdd783ad1365aa380ed576a9e66c6dc3c09f +size 31646 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png index 1ceb7c6b68..3d848e6a90 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89e17d0b8fdd749b6e97385e8369132d52b0e10adf7d23157c3bdbeb7721e694 -size 39007 +oid sha256:3d19ed9cd344be378c3ac33cc3efe8708e7803fde0234c524d83bf1a27fc2b71 +size 39320 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png index 8ca8df6e8e..cdb602aae4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c7dea18de5eabe820fe8670cd09d7b68160a97f31e4f1c474c790d829854ef7 -size 25291 +oid sha256:2c2976543550c63540f3e4533685e342ef5775018a37f5e282bf889e42c199e6 +size 24955 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png index e7f7345c31..37ecdb10ac 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98b39ded83aac1bb5befba6749c08a328b6855bcbd491c1a2f669c848ce72c31 -size 22982 +oid sha256:478c8ee2d55bb2a9f99b8d83c6e0eb0b316237ff1a60a6a735b5eb06f7ca083e +size 23029 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png index 086f166d79..148d707197 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f7aa184c28a4281a30a443b208bf3f64d9a0f85ad135ef50c999d40362c67d4 -size 24670 +oid sha256:c8eb14a3df5edff4af450b39224e6af6efce20f0734119835e6fbec210491412 +size 24251 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png index 86ab39550c..e62cdf3ec4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e13b61fcb56fbd64064bc35628957cb68507efb7fdc25280d6dbf1e6477addc -size 22637 +oid sha256:d684c9ed8b2c38cdf7017194ca656ca8da161ab0a40ef3270c6e3988d8e7f144 +size 22664 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png index a094305aad..fcf2e1f03a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f520fc8eac5e100bc4e8b0bf0c84e615078dabbdf502411e6dca5a4d579cfcab -size 12255 +oid sha256:71381d66fb321f01d6d874c025681489224626ad9700c0f15fe619f73f1a648e +size 12532 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png new file mode 100644 index 0000000000..779d297fb6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2ac4d88f51c8c1718a525a5abeccbe2bb19df75a100d504de0cb106ff3117c +size 13857 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_en.png new file mode 100644 index 0000000000..e3e7f9da05 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3eea2cd53364f9d0c85b803c8faa29f083f9db59123ba6e980a79bff7779402 +size 14113 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png index 3692cfba78..b7daa3bf21 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79fd6fba99568650dcbdac47a0ecfda6faa2cd893ded73d5ac4cbf852b59bd63 -size 11889 +oid sha256:d834acd383dfe8da50426e28e267a1076e5634fc2f96e1e7593cb6a05988600f +size 12074 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png new file mode 100644 index 0000000000..f2a9faa5ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a14ccc9724a15a7ae3f477a8cda366be345acf10b7a98ce63c4889bc1708844b +size 13379 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_2_en.png new file mode 100644 index 0000000000..bc3991b8ce --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26e4aa8610c1dede0d1b637072e773b5c46fef24bc436b9e7444e26f3ba9814c +size 13689 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en.png index 67fbefb086..ad12fe8923 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbd0574dba895e157ff5b69ab23f3b389d280538810b457d2860ae5d77331705 -size 8256 +oid sha256:2b55fce1bf0d6b764aaed8eed0fbc1416c5aa1c5b6160775a641dfa10addb227 +size 8231 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en.png index 6b140844b0..db6b026090 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e87c0990dcaf2511a2a0573a04bf14c6a9a7d24e0291fed18aa3a9ea28da572 -size 7543 +oid sha256:4ca666990cb421602e21be0dbf4ec075e8d3e94f9835464923bd31d2414d9d57 +size 7594 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png index de0bd78c7d..e40d0af8b2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ed14962b63afa972c68a53d2366b9f00906bab2f3436220bb28643fcb1d56cb -size 7668 +oid sha256:01726f688e5149a460cb1004dff0c7700302a3fe7179829603625dd369b20e78 +size 7659 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en.png index bded2caa96..65f3240329 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a50093fb47b3b17a2ccd1aeb2a10fb6b5d368add8f7d458e50f0cc8643128b7c -size 8053 +oid sha256:8c30a496415f6c00f5ce3a01c775c78994992efd68b2548ecbb5b5ffac054cc1 +size 8041 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en.png index 7030c0a8c4..039388acdc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a45336123e1eec5ea4b21c6932c1c11d6024685f736454e585a0fb6e6a85ccb2 -size 7701 +oid sha256:10f58df934f372a3b8c68c8e0c4ef61e5bfef242d65daa8f34f4999249bf9e8d +size 7799 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png index 41449d52b8..f9f1b8195d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:174f9f97fbce115d36fc5124d3301bd148444acdf24bdc933b5a3fb0754c883d -size 7545 +oid sha256:6d452b48ef65df5257eb708553d667bee46b9d83b1c29a07119c54e342cf1bc2 +size 7527 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png index 511027b648..99b5263e02 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94b71c47da614e247f1c9bbe6df50ae036c5587b964f4df460859927d4807d37 -size 6154 +oid sha256:db3ad4fbd7c140588ed40887455e3ee6b77968ca8682769826f0a447d867ac2f +size 6115 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png index cf25daf7f0..2c843d2676 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.txt_TextFileContentView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5d66ac02ac2d39125c26406f70880f2b58f3ffb88569f19b0e5104a03293034 -size 6097 +oid sha256:8b91198bf6f1cc53e13eea6c7092142800760c7103349c37ca931d722a549616 +size 6064 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png index 9b62306041..12f6ba7c36 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed8342b56d749db4d862aaa82d5d312089494965c2879adf18e8fd0c94f8525f -size 13476 +oid sha256:f10ca2d461e4078e46455e007eca2d1e9c9a20dbb6bc24c681fa5164e5f50efd +size 13531 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png index 72bd9cfb6c..6273b591af 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04a62974dd81e0786127501b8778f9d302db38a27c984d1b403884942d43accd -size 13209 +oid sha256:da386980ce6a45e102727715b5d50f219e39dcb40a513abdfaab9fa725b8ea41 +size 13260 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 index 41e45ad002..c45563c143 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e641d1d6604b6c5f489ab38438d9f3b8dcea9802113011200e9ea589c4e2dbee -size 25310 +oid sha256:5a9b7b0c427f40aa8f0a61025fdcfb8093c611e15ef10f22576545a64c6ec35c +size 25286 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 index d2231ed54c..9ca317cf9d 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:041669475888f2f0c3b2d34502ae72098547340d8c2422ea2a674d38eb6a6241 -size 207620 +oid sha256:d265f0c3fe5bc7f5a6e6d6cfa022ba16ca770b65eeb9a742ba250ff55c4b066a +size 207625 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 index 36ecce8294..2e580e55a4 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5aa3bca6cd248ac4725fb35aa11a465029dc534b8b167aedbf3e9bc240577e9c -size 654171 +oid sha256:4e8ce597c240a7e72b6811537b6fd24e1bd38714db0ce074377ab3f16eaf0436 +size 656958 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_18_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_18_en.png new file mode 100644 index 0000000000..db13b64ba8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_18_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a8582605ae13be4b4ee00b5e2156eb4fc939174744bd0ef467bb21eeffe8009 +size 679515 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_19_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_19_en.png new file mode 100644 index 0000000000..59ad8d4de8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_19_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5a5a84faafc58b6315237373a413ddbbaddd9b2495bf842882d20243daf6d50 +size 680730 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_20_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_20_en.png new file mode 100644 index 0000000000..2f1f448778 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_20_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62493d73c48e90e4bd55142594de567fb1543496c97270fe29086ad7a686557a +size 679273 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_21_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_21_en.png new file mode 100644 index 0000000000..f71ddb8758 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_21_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d3b717ce973ff874713b965cfffbd505d939539172002e0f80f6675eb38d9f5 +size 680008 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_22_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_22_en.png new file mode 100644 index 0000000000..96c74d1427 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_22_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efe53473954202132a757155217a3a7dd3c06b8f69da4437dd82757fca2110dd +size 682749 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 index c63d5a9110..cc94e583b8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd5b76aefb0fde0605556e78c7286bf8ee8dd465def0e3095ebc97ce3427eafa -size 666239 +oid sha256:d54b2bccd0f5feb6de0051d66310deee7a776b479c8982d634cf2ace2e153d0f +size 679587 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 index 8a6ebb2746..e705db22de 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c56b22d79924d1f463f01428d5be1c69c8068b94a953160fae59a9f6faa112ad -size 206047 +oid sha256:54e3011eb38f3c3e21cf64e4a9e0ee4d0c96bc0c2705975ae06dbe7422e03fdb +size 212870 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 index a092bf37ac..3afa92e837 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e23aad00065523bf1ee0e26cd094eb6a9bdd73d65f0bd9cac62cb79e7876481 -size 196533 +oid sha256:515d5a47d530117ff21cb922d1cb2cb81b22d02c083c51bbff69964f92260bbe +size 197095 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 index 619bd4e8c6..d78b959356 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d98ceaeb669342160d04783f07fbb21929d8be46d30181b60e0f09a99abfe79 -size 197127 +oid sha256:b9f402d6f56e6b9eb8d46add584ef28bf88d4a6c16ae050b7773895faafa8d38 +size 197707 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 index bbea97e0b3..6ac98e0150 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e72ef42ad838837ce2427af5677931f5db68f876eccc297894c7a2cdd437cffa -size 210729 +oid sha256:2871bd26b6c21d31ca251761eb5a203468406b4d18c13662fa815c53f015b5e1 +size 210297 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 index 67bec8b0d3..6f49e5ee89 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58a323c4f06745a877ea546870fbe69ec927aea50584001b3871cad833fca1d4 -size 211343 +oid sha256:d9dadffc89e0a66b54a0e6b94006f392569e1907dda2c6d0b6134aad187ff84a +size 210913 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png index 1ceb7c6b68..3d848e6a90 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89e17d0b8fdd749b6e97385e8369132d52b0e10adf7d23157c3bdbeb7721e694 -size 39007 +oid sha256:3d19ed9cd344be378c3ac33cc3efe8708e7803fde0234c524d83bf1a27fc2b71 +size 39320 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png index d5b81bb469..430511e490 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55671428c52d37c3f63f8b3410817d4e893e2f4327b31b227bc7b7d9ff84b884 -size 134680 +oid sha256:d298f8dfd834c532f3607ee6d23600ad40b877ccb6fe08a8297ab094a457f20c +size 134789 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 index 5b167203f5..be398e06bf 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bddbe3b0e66e0a2ce49b39321057c94427933de81663f680237a398e9929ba3 -size 442729 +oid sha256:2adfc04e0f7999ba861bf931b853f068067387558e81464e99caba1c2445a3a7 +size 444811 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_18_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_18_en.png new file mode 100644 index 0000000000..6788c98af6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_18_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9fd18a67499a6a3d9d12dea3fd5bafa45ed2e380fa0658ec0da004e50d7b6c3 +size 398823 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_19_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_19_en.png new file mode 100644 index 0000000000..86a3fd29f7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_19_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47f12ee4d355cda76dbe58bc3d6a6af64e0e076a973073ada1930195bcef0c23 +size 399533 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_20_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_20_en.png new file mode 100644 index 0000000000..3ceebdd6fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_20_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f65b6d5297d4c7c4904da07659bc8cdcb866418b787ce7a116b0b7bafef2690c +size 399331 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_21_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_21_en.png new file mode 100644 index 0000000000..3c6eb97e1f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_21_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b0009b38846b314925ab9a8ff6994fc99e156ada817a8798ba5550bd12eb8de +size 399156 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_22_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_22_en.png new file mode 100644 index 0000000000..7f27c3422a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_22_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c4d7f3fdbb0c331ef19f4481ab128b2515e6c7110b40ba2734efc44caa3620f +size 404812 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png index 167467d515..a737a9e582 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bd4a96daaa24c01b7d0007fdd14460dc80c65c13d0ccc7a887b04fd90e9fa99 -size 396805 +oid sha256:9343d4b20f7e676ff52f8f48a046d70b8762c6901ea2d6cf254eb6e447397c4a +size 398255 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png index 26dcbe4099..f318a0eb96 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7bee65567cced59131d471b5c3795347a2ef52cf64ff74b47f1c983a0b4a36f -size 130718 +oid sha256:e0b66b9b49be0cf623679062eb5d7fc8e0c26523e228522927d26ebdbee9ac4a +size 133376 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png index 31403f8980..3bcf436edd 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9cd21b6d9a9d0a9ef656bf895d6d135ba76160695355ce034fe985991d6955b -size 123949 +oid sha256:9fe5efbf527ef99de18a44e05edb9654b5bc58c17a2fd2d05f57567fbe27ef2c +size 125021 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png index bc0720e4b4..347c05b83f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2dce0794e50116043fde9f6eb76b0c05ca21fc9cee26bde01618024c24c154b2 -size 14952 +oid sha256:203bb59909a9adb94eeb51d63d8199eedee93d2f1d257572e779af9ada9a8980 +size 125404 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png index f2ff11333d..53aafed48f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3cea16c42488306fe29fd22362a0d37c1eeb0fbb1db48f26ad7ff18f6b196ae -size 137605 +oid sha256:cc65f284ec05a8ed7ea496c49c52afae585f09d26986d8582c00385459823393 +size 136782 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png index 62de7a4953..b3740e9938 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58655e1846dbe73c287073ac909ee6c881a0ad800e8852653116fdbfd043b9a0 -size 137881 +oid sha256:039749e7d726be826927bf69a89e3676a3f60ee60a07e8754b4376a08584af0f +size 137077 diff --git a/tests/uitests/src/test/snapshots/images/libraries.qrcode_QrCodeView_en.png b/tests/uitests/src/test/snapshots/images/libraries.qrcode_QrCodeView_en.png index beec069b37..7eef6a2893 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.qrcode_QrCodeView_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.qrcode_QrCodeView_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c07e9a4ec69a889b003bee4293a610c9fe0fd0f83c10c0da01322e076e9c980d -size 10750 +oid sha256:a8cba2cef4b9bfc962783765544df57930f62eea322078a10969487ce08e14be +size 6401 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png index 051db89ece..88c43c729c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d06e84c695086a799c8377bcf48869675b98dcadd1b5320e685d775589ac40c7 -size 30950 +oid sha256:e9dd0c6858e4cd885d05aedfa92797c9aae6d56fb0b60268ed59793524121465 +size 28962 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png index ddde486abc..2e4c86a4c9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3825e3c312d75a865545745ee00b64d8c2259e515dc45fad64236963e91f2427 -size 28999 +oid sha256:2f2601edb209c898a03dbbca33c963eb0ded0c8f1258f145d743b263e99b8418 +size 26945 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png index 821a37d7c0..4e53ef31de 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b0f7e2b97a343d0e1bedd9595a1057a03a4429c3fa72613451ac86a7146392a -size 33382 +oid sha256:0ec941914c0e4a5d4dfe376a6f13b4330b46eac3fc1c31708ac0122a3e6c1038 +size 31129 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png index c19c6f4fbb..dd7e2b791c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cccff3215bc81e2a6491f3b0da3f4d8997179b54c698f962eb245f535e6cc9fc -size 28655 +oid sha256:e3663eff19bb6c270af51109248836b8ee8da778538406ae2b67d10a56263d05 +size 26639 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png new file mode 100644 index 0000000000..57ea66e527 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6511dac276623acfccbdde7f8b3f79930107b5e386b8e60b3d3f5ec733a41202 +size 30781 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png index 97ff3939e2..5a6fe4a2c0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b38450dc7cc24e6f9a0db829aa11058d10ae45d433945afd534627534f362554 -size 30261 +oid sha256:014b95f94a6a0c7711146df291fb22a49fde5e98c8584bdb69f3cb4f0b626e36 +size 28375 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png index 1c3dfb9480..28efeca99a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1b36608e269c7572c93c6f10cddeaf42bfeba404c6699a6e050144bc28bc55e -size 28569 +oid sha256:7cf02e53cfcc806cf02c04ecf275abc84265401e2a4e6ce4ebfa64e1c4e0f9d4 +size 26519 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png index c4689af159..5eaab7201a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96f198461d8d60d8610f337c3dbe90bebd8c98f32a4739310532d629ca336a1f -size 33113 +oid sha256:ac935e780ea072953e6e15edb4a845605ca77776d6ad50c5782018ee5dd84d5e +size 31120 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png index 5301142de1..736e9bc843 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12350783e27c09074bdaba2e74ac445b643ff6a13251c13a7cdf98999274325d -size 27915 +oid sha256:52be506a4811614853b5c101907caddaeddde4a9147e0f6ac0a3ce31610a3612 +size 26020 diff --git a/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png new file mode 100644 index 0000000000..6c89cccb0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.roomselect.impl_RoomSelectView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a05353b7037ed03cdcdcd5821533d9cfd766e02260e9838eac8c7d86fc91e1a +size 30328 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png index 5f54605f82..96215547bb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a9022d63fa2d6f972b94856ceae281a918bddbdb935962bd21088baf126e03c -size 50165 +oid sha256:d1622f9765fda65985a369d6958f7a5993696dd629c0c1013c0a1647cfb7b562 +size 50109 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png index 996927c6c1..70d41498a8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adf6cbfa0c5a85debf5f271e16c1cd6ea2d8bf092d8a5ff829e624212805e21e -size 48034 +oid sha256:a94de003f65d2fb30ab42e4ac60802707abf1358b0defed7598a3aad69992f40 +size 48136 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png index 40342f172c..c17cbfdb66 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f8c5be5646019061349e6b3a7f358de7a0e2198f132be83cc44ddf5d82ac6da -size 51339 +oid sha256:fdab4bea33337547250355c29991f45c118e8c2c05abdf69b1824475feced5d6 +size 51241 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png index 2156c25e2a..ef9c49d6c5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:734df31d9ad63ff8ce0877daaa7c1036a0a322209d16ff9c9cf544d26676bdf9 -size 49482 +oid sha256:0f92c594febc463911c8418deeff3b94e5a8ce4171461d812a0f4f527ab0cf8e +size 49245 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png index 12323c479d..d5595c72ab 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d52e9f943bbefe5bc763dcd2b460c2e701625d427efc344b53b1ca38854bd8b -size 41761 +oid sha256:74806187f896f73b640c7401d5d0f53ec058ef788cf85d71859367057fa1ec1c +size 41383 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png index 3b24860bec..94f878d9a4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa6e7dae4a224c2911536a5fc956cdee78fb02b0af2549054dc3705c53aff481 -size 39131 +oid sha256:cefac2bbe8799850080d74b48e437320308d5e0c8f6435b113964293ffeedcc9 +size 39145 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png index d29752dfca..9ba3d99527 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d362911049fc57b2a321042c420e36a5685d29957e8da336babd9abac14d50c6 -size 50031 +oid sha256:81d8acdb1158c3e5c19b5a602e3b9afe7643f1bb77d43f51d3f5c4327478a76a +size 50282 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png index 16b16d8f75..969e5b04bf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:827d8beccea22589652fde2e1b83de48429775301f7d68c43950605b92940fb7 -size 47849 +oid sha256:c225ad12c5a32e2392606f9afacc6dbc37478a141d5485e4942012c6b22ad052 +size 48046 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png index f7c45ecf1c..a0192f0890 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c62b0036ff3375f70003b21192a2e8e05871b54c960022bac57066eb390b47a3 -size 60597 +oid sha256:3cfb1d8b7828692d5c318206770f9484822a1749b3e80a618b502811cf512d76 +size 60489 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png index 6135707a6d..c745fa256a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d393c37651e18449f5f0fdd44fdd9e91d1a2d4fbb6afbd3a92c91dba5c125135 -size 57722 +oid sha256:ca67409e4af247b43d8594d6351419cd12894485357596aff7b26745e2df009a +size 57687 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png index 728390d025..635e14be9e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c17f3c52a425eb77760c6c3d5f9e18c4dc4f14ca5fb6e4e386d9205df3b4fbb -size 49990 +oid sha256:8b3a91d104a5949dde83ac38b3127afa342816c631a1e29d78e2ada1bcc906bb +size 50150 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png index b793c0b42f..573b6123ed 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70700bd9b3b3157f75336c7d4b6f56deb462bd30bbcea6ac773c2da5f993df90 -size 47773 +oid sha256:69c281829fbca15261f196d2134f933b789ff444cd7bebc88d05cdbb7553357f +size 48012 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en.png index cfe22711ba..d50fb01949 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6af8b4ecc2845f6cc8ee6c15507852ead9bc76840e35deeeb0f0318ab9f7678e -size 61702 +oid sha256:7af3da131c81a7e76319bfbf757c331dfb342d5c556a8d6eb4234e0779b156d2 +size 61830 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en.png index d96c551efa..4074caa168 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormattingNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44d1604be0007f4db8585fd40ae56d4a346801c98388c3b69582d32be34cdd7a -size 58533 +oid sha256:023f50c3ecaaf2879d054c3a1bb825adbefbd30c611fe6400a1927180b63f75f +size 58950 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png index 1764036218..fa1fad9c0e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24debf9cd1db19d5e6245e556d871b9a2a756d6c9e80fd5b45cb930e9a50a394 -size 51429 +oid sha256:e0f8b6bdab8df8b2c1aeb70da681964724e0c2adbe4ba0cc22029de944998291 +size 51221 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png index f73f125a1a..022b7c0349 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e9ce65239d7f8cee5ace51d9559ad4433cba28d762bf535228d9c4fe6755eac -size 48818 +oid sha256:112e5cb4d2cd8f5015ab01f2c2011ae8aeb60be274a239e8bad8953d96bd2c2f +size 48537 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 e35f1b24c8..4a6e665500 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:08ea9ead35a143a4a14fa6b4185258417466d7d05659ac839c823d80a950a8f6 -size 71689 +oid sha256:ead7c23c723cbaf116629cfc502f4696454a1da1bdcb4aeaac29605d937d2605 +size 71610 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 892b7a03b0..3ee771684a 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:aa695595ef8f487bf0503648024f4337ac8b39035a79d8c5612c366dd2c8c570 -size 58177 +oid sha256:4e9d1d7021b65b18e7038e6551495b6d391381e85484827c005594fef3a038c7 +size 57984 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 a2bb96eba8..846a262c57 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:6e6e9f5972f3119468855637eb0ff4e975ffe1a36162bd88485ff34598bed6d5 -size 71258 +oid sha256:68ef756dd45911d9a06f5ceb2ce4917d0d35ed0c9f1b6aa559fccef662672e38 +size 71118 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 b1ef3a5c64..c56359f7e4 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:36d9ffefb8b575dbdf308b4075511edb2716f1b5d5de7dcb96a0a13dd68d9052 -size 79811 +oid sha256:63eb5bc48de34efb71ffa644e78f18e0596c709f153b35fb6b7aec49dbdaab78 +size 79800 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 1e939c2803..4551d86254 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:27320b5230058ae90d35f681eb46a64fcdb0a83037b2e5ec2fce49a331e4f525 -size 61091 +oid sha256:fe419a00799214722644f17edb94aa4dda3bb7c3b4f19dad689c490bf05d550d +size 60676 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 f4afd108ed..0beaf993fb 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:e159e59cc4fbcb7e33b4db5f0b04b486ab7d07426371e288c9bbc2b80c05097c -size 59854 +oid sha256:864b6a01fbcd041a2cb5fee1602fceed5f311a1d73bab6fb7b10a3acf279f47f +size 59579 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 fa92ec10e6..326f3948ed 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:db7a7db0cd732c80d03b561ed3061843bdecd23c6f188a7117d3674a62fa15e7 -size 66757 +oid sha256:59a644589a3eb4ef7d791994caf7c6aae6b23a8964405db4d8e9ebba4fc26459 +size 66594 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 eb02cd57a7..f583680c90 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:2f4d9572d105726c07387f037e65092adcbb8b631d0406eabad30dbc8382383d -size 88343 +oid sha256:7185c0bbb80d40e3da31df878f5127947e5944dcf3ed81b8266342dea94a585d +size 88177 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 d6a380f3a1..2de8f41575 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:e9dd1cb61ef6b1a6f15d766882f6d748ff8b45c4cb45588ebf8e3787cfac269c -size 59158 +oid sha256:957b3b1896978c725e03d8d8a22afb767fe032b8c10e499a70f0395eface7d12 +size 58909 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 3ccd6de45d..f97044e8b6 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:b5286b5cea91c0f474d86413a1808129cecbf4a5b5992dee2059ec0cbe6e74ee -size 59207 +oid sha256:faa3cf430f066965686bf11dc9dcedd7a7161e18f6a96be566b8f9be615ee164 +size 58974 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 b4712967f6..a2a9d84eca 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:5330b8699041f01a5874c92152f8b6524755d327612d976c7b731ac9def233e2 -size 66344 +oid sha256:f21ce912bf7ec69db5cbb024c09d385cee117e0698b28432a54fb3c330d4b44c +size 66110 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 b9cc103539..cdd4983e85 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:65f9224c7c4556477c362fe81057d3fc89fe9043e447d225809dbb66d1895f74 -size 58644 +oid sha256:47521fcbb94b38913cdffa41faef1a0af40289576a892df84fb166b855a77cec +size 58441 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 aed4467033..f89f4b12b7 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:0ec9a6e9914fcaece7a5fc79a94437a6798a9ef9fe29da301a9b6b199bc90ecc -size 68290 +oid sha256:82904fd6abba5122b5b243e88e58bbf9a5c128128999afb4b536e689f114b967 +size 68610 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 9499864bec..4ccf9c5917 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:a765b2e132737bb0213be31344fea05e60dc46b30b032c0c53b1a7adae59359c -size 55167 +oid sha256:06e63017d3636a943f015652d90089c6ded569930067dd8d6020df4053d98d3c +size 55340 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 e65958a5d4..aebee6a41b 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:c26f7171640584447f185263092281a44b25c2cb3cef9e470cd9d10474c1f241 -size 67857 +oid sha256:b4564c1019c830ef0bbed284bd9c8ac8dd1c8082dc76f89e394ae82a1756fa17 +size 68119 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 533989345d..42f7ea79a3 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:9bf5ef7638d415bedef349848c03bdcc093e00f391d91c5d528b466ee8117a64 -size 76423 +oid sha256:713af5b3d329a98e75e9587b8b228e1c017a342dc5bc4663df5d0aebf4951637 +size 76948 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 c8636a07f9..7313e32cf0 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:dc76afb5738bb1cc70ce048a990cb428dce012eebbf503df59439cbd8546dbe4 -size 58006 +oid sha256:0bf814a838410e92a932373b39aa91deb53b18857055770ffa2c1687d6128e37 +size 57989 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 838a92aeb7..a372614326 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:30b9d933c5e1cf1a5f4bc721cf8328b87ea025fae703c239a81a6d68748775d5 -size 56888 +oid sha256:728842933007c366c10e0293a22b0722bf2436c43e3ced69bb78838fef4e4aac +size 56944 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 e9afa6fd69..639cfeee1f 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:5308541d962b0835476e6443a4dd1af53bebd477ab2f382daefda9121483cff5 -size 63637 +oid sha256:55c8b429bfc0d518cde9b16127adf05fb23a29f70df2572a6212c3f837f36133 +size 63826 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 fe9f9527af..bdf1ea5885 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:c2325468ef6b2c89b2989d7ce80526cae29eaab3708da4bedcbde0b2c4c2f9dd -size 84835 +oid sha256:bb8e8cf4624d8b6a75db331dd43fb4b51df2723f98eccdc4d87bc1d4df80c3a8 +size 84918 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 139c44ba12..465d5a5c11 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:451ef300ac8c65887a1caccf643772dd331f59dd1181c97f0b9c53a0ff457f89 -size 56207 +oid sha256:9c3c802c57eeb15b14ab02677a84b422238aa08515b9151d9e01c8aefc0327b5 +size 56322 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 71a9acd0b1..3ba841381f 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:4bcfaa8bca539371c914e56ed1108282f0e4df826f99527ce982a720f22d2655 -size 56192 +oid sha256:7995253a70c47aca2215d475549c5da8a5ecd6331f8e1d2268df6979cd5fbc01 +size 56276 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 c4f985c80e..ee775246b6 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:d65864db2aa76e16df454f6772c296ac4cb9efebe2a417560e2b0e8e65503a89 -size 63211 +oid sha256:8b3868e61ecafeb234c4e3438c138ac420192a569949fc8f4f8e4c946ba42f14 +size 63243 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 6c2e72c7ce..861384a18e 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:483daeec8b5d0be508de8f979ace30bdda1ee1c7b3ccb33af0938deacb8f0f40 -size 55673 +oid sha256:1912b608b35257618b42d8abb602f031b67fcbc9810e59e6b20bc86075002b5d +size 55847 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 2114fe5594..b78d36fdd4 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:247d836caf3bed3b89c6a772861d8e302f0b9c5f9c299edd316ad83ce55c8d9e -size 73090 +oid sha256:745714bf11601a9abedc9526d3ddfcbfebc1ed8d73872ebd33f10cc638119c19 +size 72647 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 bef915901c..5ef7a3ecaa 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:b3006fc80bef839cb8a2a1ad83f9572b157f0abfe8fd9f170de2c09f882e02d0 -size 56132 +oid sha256:3a55cd98bc72c250706402bfac0706da3adc60a23010ab3a634290f2115f8543 +size 56527 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 72e28cbe9d..94488a8118 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:f3c5de24b62819c74c8183ea8a934e3e463bd2da4d00641341ade7e58b644e61 -size 71496 +oid sha256:5f4ec0a26c52319ec4c0e3468115c977bf3d4be116fa29d5f234758978b518cc +size 70973 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 5db52bc9c2..31cf97c9c4 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:b60c12e4345b74815300f992b5c95a54c9ceb4d91e511f13e98e2f2805bfa1f9 -size 82673 +oid sha256:a34d90d08ce5cd10a239c3accb0a92abeeb5309ac73a079700ded09ed1ee1697 +size 82282 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 f01e79ce79..7cb03f9aac 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:04dca8661539bd8c9a4afd97f1882415d7c27858b2b489de4f857205543be4fc -size 59397 +oid sha256:3d2c24321d4e7e827aa56ec9715d591b0f47091dab02b13c1dc045a175a70154 +size 59640 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 3146c25a3d..e008b3ee4e 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:8adec1a157ed614916988f563391dc817319bb27b10d5b4a3a41da531e9554a0 -size 58466 +oid sha256:c7727ad7b23e98365dbf7b30f2375500cdbff8062d17a1909b166040e07281a0 +size 58638 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 c6fa978c91..ba46e2e270 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:4bea8196d7650886772d346a013e85307005d9a3f24b5a681a14fc1f218b971e -size 66295 +oid sha256:b514f0f3c246531c3bab262d9b85f60eb737b960c0f1401c0c8d684d75a4f1d9 +size 66694 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 ae98e95d46..0c23280443 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:d3e535259e6d81f396970531e095453989908e91f4a8ecaf47325430eb86c955 -size 101506 +oid sha256:1a84a52cd5320adfa2bc77781d7d5b996b21a4dcc94d2800c66e636a06f40a40 +size 101578 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 ea8fccf53c..5b5b142b81 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:cc534ed58d1faf6fbf1f4fdace08e02958e60b54c457436eac2a9c1357b39517 -size 57490 +oid sha256:795ab6fd6386131a124707e96d38a916dca88fa081562cbaef269f6831fc55ca +size 57855 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 171702f3db..9de8ba3f72 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:a56524de1c9f55eb3d6d88342d6d8576508d6145b3fecb8dddc38c598c82a67c -size 57356 +oid sha256:cae668f5a1adfbe42e44ea472dbf7dc5079ec1c3826507859d3e0536766fdb39 +size 57700 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 882819289b..e7784793c4 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:0c7299aba8e5340b443223858d405f99ab1a4667721b0abbd895713111055116 -size 66570 +oid sha256:4e704bc61a2ae26cfdc30262a4e43122f24faa2a546ae7f16ad15560fc615327 +size 66960 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 b27cfcbcf3..5b56c20157 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:4294a5f0c9b943ccf3e9c5ac6f9ecbd574c34cd5334a044342c72cf72f0ef80a -size 56848 +oid sha256:919a9159a4a6b82d65e2f0a59cf55031620f32fe4066aca9440ffdd9f97ccd01 +size 57246 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 182b04ed36..b18545b512 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:d2a3c8af39fa40b72e95cc4191f820ab2b2b173e37b99178ff7c06a5870ca2c7 -size 69971 +oid sha256:758cd0c11cb3d951b7c6db787bbaf66dc87252758c6bc9b36e0d1b479fd495e8 +size 69281 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 834036c242..6ec2ca88f6 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:73a1a7607c6273081ef1b866be384fc40b65283cca4f72f701f174d913973c85 -size 52902 +oid sha256:56711e24449f15fa13b6147d1087180bc18f9a873eb0a96d19acfbaae66f74fa +size 53284 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 e35a7cd447..638136ddda 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:f5bbbd86cfe40fb6e861a9473f0a63bcbba808d915da99b14f085a27b9428e15 -size 68300 +oid sha256:b598d4cb370923b7aa28287247c08131b773aee20467dd5d3ce95d0506e26ec6 +size 67706 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 8ca87c0c40..ccab6d1cc6 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:b9610ae365f1cc089b17cb068e66c5f90414364c6bf181c1c7ea2800353a9914 -size 79614 +oid sha256:3d0300a4ba4a025b29e37cf01e29d9cd77ac411fe00642c64a9c14ff80ee4149 +size 79018 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 45d11dd4ac..c2a4773ab4 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:072afde5059b3a90977929811f43402d396ac64debf2cbf17c1f6efd35394b46 -size 56298 +oid sha256:3224a33cd9db2adf69479e8acfbaacaabd0d8ba00d4d862f1b38fc8e360539bf +size 56546 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 06d1f5c9c3..a01a166e95 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:563e94244304f75a1d64151fdc35e35a9eee2f0c88d4dd4e4872a580adfc434b -size 55392 +oid sha256:a2cbbf5adf4d45fa65aa176b55216f8dff89114ef1a5b4de57bc86a53a22f012 +size 55584 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 b10f296866..d9592ab2d0 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:529ee850c430ea3bd54aac5b0a768cf49dbce44c6629e41ca4a8293e60d993f9 -size 63140 +oid sha256:c49ee949c20152ff3c9ecef1bd361b223fc4eff62a420740d9b708343f5c6121 +size 63442 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 80fb5cec31..e6f0772abc 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:38ab31d94f2252fc5a6bfa720529eaa1d0f9459395e77383cf55b06547ca6117 -size 97622 +oid sha256:46c32f65a5071767b8603f93711f8c3c6150bfc9f6c0f6fd61317aafdebb2e83 +size 97701 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 a83053dfd8..f18f28fa0f 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:a2b367cb2f3d35dce7c038c3358906e32669245537df0e4d6ec33c21ce6c23ff -size 54362 +oid sha256:b2baad13bfc9fd755108365a9b0f7251f60cc8f9bf335115ab5c02b2d0f19f74 +size 54660 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 478b23ef50..381119052c 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:57a1fbbce28ef24a5190758b2482622fafda8ec6206b68b8ed632e0e946dda87 -size 54131 +oid sha256:0a735f4a7003f631c230c29e4248d186d86111ada2c4c4cfc8e6e4fe9897b224 +size 54424 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 93ddb25b2b..16da419a33 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:261747f96fbf93367dae4819a2d4ae8883573cc75af1d6f43cae77f0eaf55736 -size 63307 +oid sha256:c109f03989aaa14873823c230bb262add95099b44031512ee29b9f8c8e7d180f +size 63642 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 d142023a66..dfa1308111 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:e5786c4a4e98207f229f41a32defb71e059ac3ea9380acacd8883e188c68e81c -size 53735 +oid sha256:bbe7caa3bd8b8788befae4c4febbcd1bb02bd2ea9d6feb00bffb962d0f2885d6 +size 54080 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png index 032729bb26..0dfda89690 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a33faaf5ea09a1d4eb3298d135211bbddc6637fd0c435f4df16874a06f184142 -size 53823 +oid sha256:77f29e3b660aafe3093d502c415129fefb08b355c39b2c6f53ddf37107f3041d +size 53424 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png index dd156cd2c7..4550c1381e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimpleNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c83a621b7bd32ab83639acc7471e9a58a2e591b6637fb09bb74c2186791ed61 -size 51273 +oid sha256:305043a2166b60160feeea6ec850050c3709f23a3c6c077851e1b09485dcdd55 +size 51126 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png index b163b855c4..41291fd5a3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c801a2ffb192267c9ade286bff5640e2fb03002491de90683f27342dcb692f5b -size 43922 +oid sha256:385ad87d0aee7ae8df87eaf8992b347292ae4ec7a0f1e2ee9a64641a03e13b61 +size 43769 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png index 2ea71b91ae..ea322a5dc3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33f43f935a457a85584cf682cbaee153128f0a21808caa8f157e542dcb81ef92 -size 41473 +oid sha256:64c015418356283f177cb8d271c97b289170e48d59551a2c20e953247d93c8c4 +size 41623 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png index e5c8277d21..466fc6d023 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b9ed5a39a881471e8b224edbcc45d46a20db0539e065c44a5eb0d56a24efc22 -size 34439 +oid sha256:08f1c0136f60903f2ef4d21aed359ec83235d73a65ea16f3ea0bc4a3e3f8d9e6 +size 34008 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png index 242f4f2874..90a5145263 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:571383a1822fb9cd4eb629d28260556172ff40a8260c9d05278434b2383a11da -size 32594 +oid sha256:94b7df69c90b93737b9eda490814932c00a7c1c441c1cadfc60196b58769200c +size 32222 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png index 72c221feb6..138c59e513 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9036efabbf937551b240326a092f037398ee2a9586203cf1c05a0725200bf3bb -size 24068 +oid sha256:84542c2e193aa926db19d6ec96f2b49e70954c300725d516b11694353239d9cf +size 24153 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png index 2ce6386e00..9f514b6a3e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ebcd0a5c857202d82a3685551cde4af4abe48883b5b19296188d8130dba295f -size 22622 +oid sha256:72ecb2b3959113e2f86db8cb0c59ad96f1661608b3fcc52b743a6086e31833ef +size 22678 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png index fd13ba4166..be602ba91f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888f6261561d7df42578e39122b152238cfc75537c964fc5bed1d69383bb263f -size 44064 +oid sha256:b8db643953d2fe589896b382a490a36961146bb0693fcbf39c108d3b6706eac1 +size 44063 diff --git a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png index e3bf16c7df..d53897b657 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.troubleshoot.impl.history_PushHistoryView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9061f0465a3a62581721d7acb6024b5a5375cd9a9f40b1153a65fa9ce42795d -size 42827 +oid sha256:17bc49574b4f43610c7c8269bd9fce440a2abc3d3396a4dcc62f133decad48b6 +size 42828 diff --git a/tools/adb/callLinkCustomScheme.sh b/tools/adb/callLinkCustomScheme.sh deleted file mode 100755 index 7e6c9f02d3..0000000000 --- a/tools/adb/callLinkCustomScheme.sh +++ /dev/null @@ -1,14 +0,0 @@ -#! /bin/bash - -# Copyright (c) 2025 Element Creations Ltd. -# Copyright 2023-2024 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. - -# Format is: -# element://call?url=some-encoded-url -# For instance -# element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall - -adb shell am start -a android.intent.action.VIEW -d element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall diff --git a/tools/adb/callLinkCustomScheme2.sh b/tools/adb/callLinkCustomScheme2.sh deleted file mode 100755 index 43f427f22f..0000000000 --- a/tools/adb/callLinkCustomScheme2.sh +++ /dev/null @@ -1,14 +0,0 @@ -#! /bin/bash - -# Copyright (c) 2025 Element Creations Ltd. -# Copyright 2023-2024 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. - -# Format is: -# io.element.call:/?url=some-encoded-url -# For instance -# io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall - -adb shell am start -a android.intent.action.VIEW -d io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 552b46755e..cc99ecab5a 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -150,7 +150,7 @@ exceptions: TooGenericExceptionCaught: active: false SwallowedException: - active: true + active: false ThrowingExceptionsWithoutMessageOrCause: active: true TooGenericExceptionThrown: @@ -224,12 +224,27 @@ comments: Compose: CompositionLocalAllowlist: active: true + # You can optionally define a list of CompositionLocals that are allowed here + allowedCompositionLocals: + - LocalCompoundColors + - LocalSnackbarDispatcher + - LocalCameraPositionState + - LocalMediaItemPresenterFactories + - LocalTimelineItemPresenterFactories + - LocalRoomMemberProfilesCache + - LocalMentionSpanUpdater + - LocalAnalyticsService + - LocalBuildMeta + - LocalUiTestMode + - LocalSdkIntVersionProvider CompositionLocalNaming: active: true ContentEmitterReturningValues: active: true # You can optionally add your own composables here # contentEmitters: MyComposable,MyOtherComposable + ModifierComposable: + active: true ModifierMissing: active: true ModifierReused: @@ -252,6 +267,8 @@ Compose: active: true PreviewPublic: active: true + # You can optionally disable that only previews with @PreviewParameter are flagged + previewPublicOnlyIfParams: false RememberMissing: active: true UnstableCollections: diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b38268e5f7..93e1c71759 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -163,7 +163,9 @@ "screen_qr_code_login_connection_note_secure_state.*", "screen_qr_code_login_unknown_error_description", "screen_qr_code_login_invalid_scan_state_.*", - "screen_qr_code_login_no_camera_permission_state_.*" + "screen_qr_code_login_no_camera_permission_state_.*", + "screen_qr_code_login_device_code_.*", + "screen_qr_code_login_verify_code_loading" ] }, { @@ -219,6 +221,7 @@ "screen_notification_settings_mentions_only_disclaimer", "screen_room_change_.*", "screen_room_roles_.*", + "screen_roomlist_mark_as_.*", "screen\\.edit_room_address\\..*", "screen\\.security_and_privacy\\..*" ] @@ -270,6 +273,7 @@ "screen_room_timeline.*", "screen\\.room_timeline.*", "screen_room_typing.*", + "screen\\.image_edition\\..*", "screen\\.media_upload.*" ] }, @@ -313,6 +317,7 @@ "screen_chat_backup_.*", "screen_key_backup_disable_.*", "screen_recovery_key_.*", + "screen\\.custom_recovery_key\\..*", "screen_create_new_recovery_key_.*", "screen_encryption_reset.*", "screen_reset_encryption.*", @@ -326,9 +331,11 @@ "screen\\.advanced_settings\\..*", "screen_edit_profile_.*", "screen_notification_settings_.*", + "screen\\.notification_settings\\..*", "screen_blocked_users_.*", "full_screen_intent_banner_.*", "troubleshoot_notifications_entry_point_.*", + "theme\\..*", "screen\\.labs\\..*" ] }, diff --git a/tools/sdk/build-rust-sdk b/tools/sdk/build-rust-sdk index eddff9f7e4..91c6ed6e02 100755 --- a/tools/sdk/build-rust-sdk +++ b/tools/sdk/build-rust-sdk @@ -27,6 +27,7 @@ set -u buildLocal=0 rustSdkPath="../matrix-rust-sdk/" +kotlinComponentsPath="../matrix-rust-components-kotlin" rustSdkUrl="https://github.com/matrix-org/matrix-rust-sdk.git" rustSdkBranch="main" buildApp=1 @@ -38,6 +39,7 @@ default_arch="${default_arch/arm64/aarch64}" target_arch="${default_arch}" sdkArg="" +update_components=1 ## Argument parsing @@ -48,7 +50,7 @@ else GNU_GETOPT="getopt" fi -TEMP=$("$GNU_GETOPT" -o 'rs:b:at:h' --long 'remote,sdk:,branch:,build-app,target-arch:,help' -- "$@") +TEMP=$("$GNU_GETOPT" -o 'rs:b:at:nh' --long 'remote,sdk:,branch:,build-app,target-arch:,no-update-components,help' -- "$@") if [ $? -ne 0 ]; then echo 'Terminating...' >&2 @@ -85,6 +87,11 @@ while true; do shift 2 continue ;; + '-n'|'--no-update-components') + update_components=0 + shift + continue + ;; '-h'|'--help') cat << END SYNOPSIS @@ -110,6 +117,9 @@ ARGUMENTS -t --target-arch The architecture for which to build the app. Defaults to the architecture of this machine (${default_arch}). + -n --no-update-components + Do not attempt to update matrix-rust-components-kotlin from git. It must already be cloned and up-to-date. + EXAMPLES $0 @@ -194,21 +204,25 @@ cd "${elementPwd}" ## Clone matrix-rust-components-kotlin if needed -if [ ! -d "../matrix-rust-components-kotlin" ]; then - printf "\nFolder ../matrix-rust-components-kotlin does not exist." - printf "Cloning the repository into ../matrix-rust-components-kotlin.\n\n" - git clone \ - https://github.com/matrix-org/matrix-rust-components-kotlin.git \ - ../matrix-rust-components-kotlin +if [ "$update_components" == "1" ]; then + if [ ! -d "${kotlinComponentsPath}" ]; then + printf "\nFolder ${kotlinComponentsPath} does not exist." + printf "Cloning the repository into ${kotlinComponentsPath}.\n\n" + git clone \ + https://github.com/matrix-org/matrix-rust-components-kotlin.git \ + "${kotlinComponentsPath}" + fi + + printf "\n## Resetting ${kotlinComponentsPath} to the latest main...\n\n" + + cd "${kotlinComponentsPath}" + git reset --hard + git checkout main + git pull +else + cd "${kotlinComponentsPath}" fi -printf "\n## Resetting matrix-rust-components-kotlin to the latest main...\n\n" - -cd ../matrix-rust-components-kotlin -git reset --hard -git checkout main -git pull - ## Build the SDK printf "\n## Building the SDK for ${target_arch}...\n\n"